添加系统管理、登录模块的接口、标准化开发流程
This commit is contained in:
148
src/components/common/IconPicker.vue
Normal file
148
src/components/common/IconPicker.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<a-popover trigger="click" placement="bottomLeft" v-model:open="visible" overlay-class-name="icon-picker-overlay">
|
||||
<template #content>
|
||||
<div class="icon-picker-content">
|
||||
<a-form-item-rest>
|
||||
<a-input-search
|
||||
v-model:value="searchValue"
|
||||
placeholder="搜索图标"
|
||||
allow-clear
|
||||
class="icon-search"
|
||||
/>
|
||||
</a-form-item-rest>
|
||||
<div class="icon-list">
|
||||
<div
|
||||
v-for="icon in filteredIcons"
|
||||
:key="icon"
|
||||
class="icon-item"
|
||||
:class="{ active: modelValue === icon }"
|
||||
@click="handleSelect(icon)"
|
||||
:title="icon"
|
||||
>
|
||||
<component :is="Icons[icon as keyof typeof Icons]" />
|
||||
</div>
|
||||
<div v-if="filteredIcons.length === 0" class="no-data">
|
||||
未找到图标
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="modelValue"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
class="icon-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<span v-if="modelValue && Icons[modelValue as keyof typeof Icons]" class="selected-icon">
|
||||
<component :is="Icons[modelValue as keyof typeof Icons]" />
|
||||
</span>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<SettingOutlined v-if="!modelValue" class="placeholder-icon" />
|
||||
<CloseCircleOutlined v-else class="clear-icon" @click.stop="handleClear" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import * as Icons from '@ant-design/icons-vue'
|
||||
import { SettingOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
value?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:value', 'change'])
|
||||
|
||||
const visible = ref(false)
|
||||
const searchValue = ref('')
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.value,
|
||||
set: (val) => {
|
||||
emit('update:value', val)
|
||||
emit('change', val)
|
||||
}
|
||||
})
|
||||
|
||||
// Filter outline icons mainly, limit list for performance
|
||||
const allIcons = Object.keys(Icons).filter(k =>
|
||||
['Outlined'].some(s => k.endsWith(s)) &&
|
||||
!['createFromIconfontCN', 'getTwoToneColor', 'setTwoToneColor', 'default'].includes(k)
|
||||
)
|
||||
|
||||
const filteredIcons = computed(() => {
|
||||
if (!searchValue.value) return allIcons.slice(0, 100)
|
||||
return allIcons.filter(k => k.toLowerCase().includes(searchValue.value.toLowerCase())).slice(0, 100)
|
||||
})
|
||||
|
||||
function handleSelect(icon: string) {
|
||||
modelValue.value = icon
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
modelValue.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-picker-content {
|
||||
width: 320px;
|
||||
}
|
||||
.icon-search {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.icon-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
.icon-item {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.icon-item:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
.icon-item.active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
.selected-icon {
|
||||
font-size: 16px;
|
||||
color: #1890ff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.placeholder-icon {
|
||||
color: #ccc;
|
||||
}
|
||||
.clear-icon {
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
.clear-icon:hover {
|
||||
color: #999;
|
||||
}
|
||||
.no-data {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
124
src/components/finance/budget/BudgetDetailModal.vue
Normal file
124
src/components/finance/budget/BudgetDetailModal.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="预算详情"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<template v-if="record">
|
||||
<a-descriptions bordered :column="2">
|
||||
<a-descriptions-item label="预算名称" :span="2">{{ record.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="预算周期">
|
||||
{{ BudgetPeriodMap[record.period] }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="时间">
|
||||
{{ record.year }}年
|
||||
{{ record.period === 'monthly' ? `${record.month}月` :
|
||||
record.period === 'quarterly' ? `Q${record.quarter}` : '' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属部门">{{ record.departmentName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getBudgetStatusColor(record.status)">
|
||||
{{ BudgetStatusMap[record.status] }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="总预算">
|
||||
<span class="money-primary">¥{{ record.totalBudget.toLocaleString() }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行率">
|
||||
<a-progress
|
||||
:percent="record.usageRate"
|
||||
:stroke-color="getProgressColor(record.usageRate)"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-divider>预算明细</a-divider>
|
||||
<a-table
|
||||
:columns="detailColumns"
|
||||
:data-source="record.items"
|
||||
:pagination="false"
|
||||
row-key="expenseType"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record: item }">
|
||||
<template v-if="column.key === 'expenseType'">
|
||||
<a-tag :color="ExpenseTypeColorMap[item.expenseType]">
|
||||
{{ ExpenseTypeMap[item.expenseType] }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'budgetAmount'">
|
||||
¥{{ item.budgetAmount.toLocaleString() }}
|
||||
</template>
|
||||
<template v-if="column.key === 'usedAmount'">
|
||||
<span class="text-warning">¥{{ item.usedAmount.toLocaleString() }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'remainingAmount'">
|
||||
<span :class="item.remainingAmount >= 0 ? 'text-success' : 'text-danger'">
|
||||
¥{{ item.remainingAmount.toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'usage'">
|
||||
<a-progress
|
||||
:percent="item.budgetAmount > 0 ? Math.round((item.usedAmount / item.budgetAmount) * 100) : 0"
|
||||
size="small"
|
||||
:stroke-color="getProgressColor(item.budgetAmount > 0 ? Math.round((item.usedAmount / item.budgetAmount) * 100) : 0)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { BudgetRecord, BudgetStatus } from '@/types/finance/budget'
|
||||
import { BudgetPeriodMap, BudgetStatusMap } from '@/types/finance/budget'
|
||||
import { ExpenseTypeMap, ExpenseTypeColorMap } from '@/types/finance/common'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
record?: BudgetRecord
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
const detailColumns = [
|
||||
{ title: '费用类型', key: 'expenseType', width: 150 },
|
||||
{ title: '预算金额', key: 'budgetAmount', width: 130, align: 'right' as const },
|
||||
{ title: '已使用', key: 'usedAmount', width: 130, align: 'right' as const },
|
||||
{ title: '剩余', key: 'remainingAmount', width: 130, align: 'right' as const },
|
||||
{ title: '使用率', key: 'usage', width: 150 }
|
||||
]
|
||||
|
||||
function getBudgetStatusColor(status: BudgetStatus): string {
|
||||
const colors: Record<BudgetStatus, string> = {
|
||||
draft: 'default',
|
||||
active: 'green',
|
||||
completed: 'blue',
|
||||
cancelled: 'red'
|
||||
}
|
||||
return colors[status]
|
||||
}
|
||||
|
||||
function getProgressColor(percent: number): string {
|
||||
if (percent >= 90) return '#ff4d4f'
|
||||
if (percent >= 70) return '#faad14'
|
||||
if (percent >= 50) return '#1890ff'
|
||||
return '#52c41a'
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.money-primary { color: #1890ff; font-weight: 600; }
|
||||
.text-success { color: #52c41a; }
|
||||
.text-warning { color: #faad14; }
|
||||
.text-danger { color: #ff4d4f; }
|
||||
</style>
|
||||
301
src/components/finance/budget/BudgetFormModal.vue
Normal file
301
src/components/finance/budget/BudgetFormModal.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:title="title"
|
||||
:confirm-loading="loading"
|
||||
width="800px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 19 }"
|
||||
>
|
||||
<a-form-item label="预算名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入预算名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="预算周期" name="period">
|
||||
<a-radio-group v-model:value="formData.period">
|
||||
<a-radio-button v-for="(label, key) in BudgetPeriodMap" :key="key" :value="key">
|
||||
{{ label }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="年度" name="year">
|
||||
<a-select v-model:value="formData.year" style="width: 120px">
|
||||
<a-select-option :value="2024">2024年</a-select-option>
|
||||
<a-select-option :value="2025">2025年</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="月份" v-if="formData.period === 'monthly'">
|
||||
<a-select v-model:value="formData.month" style="width: 120px">
|
||||
<a-select-option v-for="m in 12" :key="m" :value="m">{{ m }}月</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="季度" v-if="formData.period === 'quarterly'">
|
||||
<a-select v-model:value="formData.quarter" style="width: 120px">
|
||||
<a-select-option v-for="q in 4" :key="q" :value="q">Q{{ q }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属部门">
|
||||
<a-select v-model:value="formData.departmentId" placeholder="选择部门(选填)" allow-clear>
|
||||
<a-select-option v-for="dept in departments" :key="dept.id" :value="dept.id">
|
||||
{{ dept.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 预算明细 -->
|
||||
<a-divider>预算明细</a-divider>
|
||||
<a-form-item label="费用预算" required>
|
||||
<div class="budget-items">
|
||||
<a-row :gutter="16" class="budget-item-header">
|
||||
<a-col :span="8">费用类型</a-col>
|
||||
<a-col :span="8">预算金额</a-col>
|
||||
<a-col :span="8">已使用</a-col>
|
||||
</a-row>
|
||||
<div v-for="(item, index) in formData.items" :key="index" class="budget-item-row">
|
||||
<a-row :gutter="16" align="middle">
|
||||
<a-col :span="8">
|
||||
<a-tag :color="ExpenseTypeColorMap[item.expenseType]">
|
||||
{{ ExpenseTypeMap[item.expenseType] }}
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-input-number
|
||||
v-model:value="item.budgetAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
:formatter="(value: any) => `¥ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
|
||||
:parser="(value: any) => value.replace(/¥\s?|(,*)/g, '')"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<span class="used-amount">¥{{ (item.usedAmount || 0).toLocaleString() }}</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<a-divider dashed />
|
||||
<a-row :gutter="16" class="budget-total-row">
|
||||
<a-col :span="8"><strong>合计</strong></a-col>
|
||||
<a-col :span="8">
|
||||
<strong class="total-budget">¥{{ calculatedTotal.toLocaleString() }}</strong>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<strong class="total-used">¥{{ calculatedUsed.toLocaleString() }}</strong>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注">
|
||||
<a-textarea v-model:value="formData.remark" :rows="2" placeholder="备注信息(选填)" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { message, type FormInstance } from 'ant-design-vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import type { BudgetRecord, BudgetPeriod, BudgetItem } from '@/types/finance/budget'
|
||||
import { BudgetPeriodMap } from '@/types/finance/budget'
|
||||
import type { ExpenseType } from '@/types/finance/common'
|
||||
import { ExpenseTypeMap, ExpenseTypeColorMap } from '@/types/finance/common'
|
||||
import { createBudget, updateBudget } from '@/api/finance/budget'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
record?: BudgetRecord
|
||||
departments: Array<{ id: number; name: string }>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const expenseTypes: ExpenseType[] = ['salary', 'office', 'travel', 'marketing', 'equipment', 'other']
|
||||
|
||||
interface FormBudgetItem {
|
||||
expenseType: ExpenseType
|
||||
budgetAmount: number
|
||||
usedAmount: number
|
||||
remainingAmount: number
|
||||
}
|
||||
|
||||
const formData = reactive({
|
||||
id: undefined as number | undefined,
|
||||
name: '',
|
||||
period: 'monthly' as BudgetPeriod,
|
||||
year: 2024,
|
||||
month: new Date().getMonth() + 1,
|
||||
quarter: Math.ceil((new Date().getMonth() + 1) / 3),
|
||||
departmentId: undefined as number | undefined,
|
||||
items: [] as FormBudgetItem[],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const title = computed(() => formData.id ? '编辑预算' : '新增预算')
|
||||
|
||||
const formRules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '请输入预算名称', trigger: 'blur' }],
|
||||
period: [{ required: true, message: '请选择预算周期', trigger: 'change' }],
|
||||
year: [{ required: true, message: '请选择年度', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const calculatedTotal = computed(() =>
|
||||
formData.items.reduce((sum, item) => sum + (item.budgetAmount || 0), 0)
|
||||
)
|
||||
|
||||
const calculatedUsed = computed(() =>
|
||||
formData.items.reduce((sum, item) => sum + (item.usedAmount || 0), 0)
|
||||
)
|
||||
|
||||
function initFormItems() {
|
||||
formData.items = expenseTypes.map(type => ({
|
||||
expenseType: type,
|
||||
budgetAmount: 0,
|
||||
usedAmount: 0,
|
||||
remainingAmount: 0
|
||||
}))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (props.record) {
|
||||
const record = props.record
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
period: record.period,
|
||||
year: record.year,
|
||||
month: record.month || 1,
|
||||
quarter: record.quarter || 1,
|
||||
departmentId: record.departmentId,
|
||||
remark: record.remark || ''
|
||||
})
|
||||
|
||||
// Map items and fill missing
|
||||
const existingItemsMap = new Map(record.items.map(item => [item.expenseType, item]))
|
||||
formData.items = expenseTypes.map(type => {
|
||||
const exist = existingItemsMap.get(type)
|
||||
return {
|
||||
expenseType: type,
|
||||
budgetAmount: exist?.budgetAmount || 0,
|
||||
usedAmount: exist?.usedAmount || 0,
|
||||
remainingAmount: exist?.remainingAmount || 0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
period: 'monthly',
|
||||
year: 2024,
|
||||
month: new Date().getMonth() + 1,
|
||||
quarter: Math.ceil((new Date().getMonth() + 1) / 3),
|
||||
departmentId: undefined,
|
||||
remark: ''
|
||||
})
|
||||
initFormItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function handleOk() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
// Prepare items
|
||||
const items: BudgetItem[] = formData.items
|
||||
.filter(item => item.budgetAmount > 0) // Only save non-zero items? Or all? User logic was > 0
|
||||
.map(item => ({
|
||||
expenseType: item.expenseType,
|
||||
budgetAmount: item.budgetAmount,
|
||||
usedAmount: item.usedAmount,
|
||||
remainingAmount: item.budgetAmount - item.usedAmount
|
||||
}))
|
||||
|
||||
const dept = props.departments.find(d => d.id === formData.departmentId)
|
||||
|
||||
const saveData: Partial<BudgetRecord> = {
|
||||
id: formData.id,
|
||||
name: formData.name,
|
||||
period: formData.period,
|
||||
year: formData.year,
|
||||
month: formData.period === 'monthly' ? formData.month : undefined,
|
||||
quarter: formData.period === 'quarterly' ? formData.quarter : undefined,
|
||||
departmentId: formData.departmentId,
|
||||
departmentName: dept?.name,
|
||||
items,
|
||||
totalBudget: calculatedTotal.value,
|
||||
usedAmount: calculatedUsed.value,
|
||||
remainingAmount: calculatedTotal.value - calculatedUsed.value,
|
||||
usageRate: calculatedTotal.value > 0 ? Math.round((calculatedUsed.value / calculatedTotal.value) * 100) : 0,
|
||||
remark: formData.remark,
|
||||
}
|
||||
|
||||
if (saveData.id) {
|
||||
await updateBudget(saveData)
|
||||
message.success('编辑成功')
|
||||
} else {
|
||||
await createBudget(saveData)
|
||||
message.success('新增成功')
|
||||
}
|
||||
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.budget-items {
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.budget-item-header {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.budget-item-row {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
}
|
||||
|
||||
.used-amount {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.budget-total-row {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.total-budget { color: #1890ff; font-size: 16px; }
|
||||
.total-used { color: #faad14; font-size: 16px; }
|
||||
</style>
|
||||
131
src/components/system/dict/DictFormModal.vue
Normal file
131
src/components/system/dict/DictFormModal.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:title="isEdit ? '编辑字典' : '新增字典'"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="submitLoading"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="字典名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入字典名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字典编码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入字典编码" :disabled="isEdit" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="3" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">正常</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
|
||||
// 字典类型定义
|
||||
interface DictRecord {
|
||||
id?: number
|
||||
name: string
|
||||
code: string
|
||||
remark?: string
|
||||
status: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
editData?: DictRecord | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive<DictRecord>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入字典名称' }],
|
||||
code: [{ required: true, message: '请输入字典编码' }]
|
||||
}
|
||||
|
||||
// 监听 editData 变化,初始化表单
|
||||
watch(
|
||||
() => props.editData,
|
||||
(val) => {
|
||||
if (val) {
|
||||
isEdit.value = true
|
||||
formData.id = val.id
|
||||
formData.name = val.name
|
||||
formData.code = val.code
|
||||
formData.remark = val.remark || ''
|
||||
formData.status = val.status
|
||||
} else {
|
||||
isEdit.value = false
|
||||
formData.id = undefined
|
||||
formData.name = ''
|
||||
formData.code = ''
|
||||
formData.remark = ''
|
||||
formData.status = 1
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听 visible,重置表单
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && !props.editData) {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
// TODO: 接入真实API
|
||||
// if (isEdit.value) {
|
||||
// await updateDict(formData)
|
||||
// } else {
|
||||
// await createDict(formData)
|
||||
// }
|
||||
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
// 验证失败
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
277
src/components/system/dict/DictItemDrawer.vue
Normal file
277
src/components/system/dict/DictItemDrawer.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:open="visible"
|
||||
:title="`字典项管理 - ${dictData?.name || ''}`"
|
||||
width="800"
|
||||
:destroyOnClose="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="drawer-content">
|
||||
<!-- 字典项操作栏 -->
|
||||
<div class="item-toolbar">
|
||||
<a-button type="primary" @click="handleAddItem">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增字典项
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 字典项表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'" size="small">
|
||||
{{ record.status === 1 ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'isDefault'">
|
||||
<a-tag v-if="record.isDefault === 1" color="blue">是</a-tag>
|
||||
<span v-else class="text-muted">否</span>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEditItem(record)">编辑</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除此字典项吗?"
|
||||
@confirm="handleDeleteItem(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑字典项弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="itemModalVisible"
|
||||
:title="isEditItem ? '编辑字典项' : '新增字典项'"
|
||||
@ok="handleSubmitItem"
|
||||
:confirm-loading="submitLoading"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="字典项标签" name="label">
|
||||
<a-input v-model:value="formData.label" placeholder="请输入显示的文本" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字典项值" name="value">
|
||||
<a-input v-model:value="formData.value" placeholder="请输入存储的值" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="formData.sort" :min="0" :max="999" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否默认" name="isDefault">
|
||||
<a-switch v-model:checked="formData.isDefault" :checked-value="1" :un-checked-value="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">正常</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
|
||||
// 类型定义
|
||||
interface DictRecord {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
interface DictItemRecord {
|
||||
id?: number
|
||||
dictId?: number
|
||||
label: string
|
||||
value: string
|
||||
sort: number
|
||||
isDefault: number
|
||||
status: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
dictData?: DictRecord | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
// 表格
|
||||
const loading = ref(false)
|
||||
const tableData = ref<DictItemRecord[]>([])
|
||||
|
||||
const columns = [
|
||||
{ title: '标签', dataIndex: 'label', width: 120 },
|
||||
{ title: '值', dataIndex: 'value', width: 100 },
|
||||
{ title: '排序', dataIndex: 'sort', width: 70 },
|
||||
{ title: '默认', dataIndex: 'isDefault', width: 70 },
|
||||
{ title: '状态', dataIndex: 'status', width: 70 },
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '操作', dataIndex: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
|
||||
// 字典项表单
|
||||
const itemModalVisible = ref(false)
|
||||
const isEditItem = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive<DictItemRecord>({
|
||||
id: undefined,
|
||||
dictId: undefined,
|
||||
label: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
isDefault: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
label: [{ required: true, message: '请输入字典项标签' }],
|
||||
value: [{ required: true, message: '请输入字典项值' }]
|
||||
}
|
||||
|
||||
// 监听 visible 变化,加载数据
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.dictData) {
|
||||
loadData(props.dictData.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 加载字典项数据
|
||||
async function loadData(dictId: number) {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 接入真实API
|
||||
// const res = await getDictItemList(dictId)
|
||||
// tableData.value = res.data.data
|
||||
|
||||
// 模拟数据
|
||||
if (dictId === 1) {
|
||||
tableData.value = [
|
||||
{ id: 1, dictId: 1, label: '男', value: '1', sort: 1, isDefault: 1, status: 1, remark: '' },
|
||||
{ id: 2, dictId: 1, label: '女', value: '2', sort: 2, isDefault: 0, status: 1, remark: '' },
|
||||
{ id: 3, dictId: 1, label: '未知', value: '0', sort: 3, isDefault: 0, status: 1, remark: '' }
|
||||
]
|
||||
} else if (dictId === 2) {
|
||||
tableData.value = [
|
||||
{ id: 4, dictId: 2, label: '正常', value: '1', sort: 1, isDefault: 1, status: 1, remark: '' },
|
||||
{ id: 5, dictId: 2, label: '禁用', value: '0', sort: 2, isDefault: 0, status: 1, remark: '' }
|
||||
]
|
||||
} else if (dictId === 3) {
|
||||
tableData.value = [
|
||||
{ id: 6, dictId: 3, label: '待审批', value: 'pending', sort: 1, isDefault: 1, status: 1, remark: '' },
|
||||
{ id: 7, dictId: 3, label: '已通过', value: 'approved', sort: 2, isDefault: 0, status: 1, remark: '' },
|
||||
{ id: 8, dictId: 3, label: '已驳回', value: 'rejected', sort: 3, isDefault: 0, status: 1, remark: '' }
|
||||
]
|
||||
} else {
|
||||
tableData.value = []
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddItem() {
|
||||
isEditItem.value = false
|
||||
formData.id = undefined
|
||||
formData.dictId = props.dictData?.id
|
||||
formData.label = ''
|
||||
formData.value = ''
|
||||
formData.sort = 0
|
||||
formData.isDefault = 0
|
||||
formData.status = 1
|
||||
formData.remark = ''
|
||||
itemModalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEditItem(record: DictItemRecord) {
|
||||
isEditItem.value = true
|
||||
formData.id = record.id
|
||||
formData.dictId = record.dictId
|
||||
formData.label = record.label
|
||||
formData.value = record.value
|
||||
formData.sort = record.sort
|
||||
formData.isDefault = record.isDefault
|
||||
formData.status = record.status
|
||||
formData.remark = record.remark || ''
|
||||
itemModalVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDeleteItem(id: number) {
|
||||
// TODO: 接入真实API
|
||||
// await deleteDictItem(id)
|
||||
message.success('删除成功')
|
||||
if (props.dictData) {
|
||||
loadData(props.dictData.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitItem() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
// TODO: 接入真实API
|
||||
// if (isEditItem.value) {
|
||||
// await updateDictItem(formData)
|
||||
// } else {
|
||||
// await createDictItem(formData)
|
||||
// }
|
||||
|
||||
message.success(isEditItem.value ? '编辑成功' : '新增成功')
|
||||
itemModalVisible.value = false
|
||||
if (props.dictData) {
|
||||
loadData(props.dictData.id)
|
||||
}
|
||||
} catch (error) {
|
||||
// 验证失败
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drawer-content {
|
||||
height: 100%;
|
||||
}
|
||||
.item-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
172
src/components/system/menu/MenuFormModal.vue
Normal file
172
src/components/system/menu/MenuFormModal.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:title="title"
|
||||
:confirm-loading="loading"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 5 }" :wrapper-col="{ span: 17 }">
|
||||
<a-form-item label="上级菜单" name="parentId">
|
||||
<a-tree-select
|
||||
v-model:value="formData.parentId"
|
||||
:tree-data="parentTreeData"
|
||||
:field-names="{ label: 'name', value: 'id', children: 'children' }"
|
||||
placeholder="请选择上级菜单(不选则为顶级)"
|
||||
tree-default-expand-all
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单类型" name="type">
|
||||
<a-radio-group v-model:value="formData.type">
|
||||
<a-radio value="directory">目录</a-radio>
|
||||
<a-radio value="menu">菜单</a-radio>
|
||||
<a-radio value="button">按钮</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入菜单名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单编码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入菜单编码(唯一标识)" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formData.type !== 'button'" label="路由路径" name="path">
|
||||
<a-input v-model:value="formData.path" placeholder="请输入路由路径,如 /system/users" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formData.type === 'menu'" label="组件路径" name="component">
|
||||
<a-input v-model:value="formData.component" placeholder="请输入组件路径" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formData.type !== 'button'" label="图标" name="icon">
|
||||
<IconPicker v-model:value="formData.icon" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formData.type === 'button'" label="权限标识" name="permission">
|
||||
<a-input v-model:value="formData.permission" placeholder="请输入权限标识,如 system:user:add" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="formData.sort" :min="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">正常</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formData.type !== 'button'" label="是否隐藏" name="hidden">
|
||||
<a-radio-group v-model:value="formData.hidden">
|
||||
<a-radio :value="0">显示</a-radio>
|
||||
<a-radio :value="1">隐藏</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { createMenu, updateMenu } from '@/api/system/menu'
|
||||
import type { MenuFormData, MenuRecord } from '@/types/system/menu'
|
||||
import IconPicker from '@/components/common/IconPicker.vue'
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
record?: MenuFormData
|
||||
menuTree: MenuRecord[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive<MenuFormData>({
|
||||
parentId: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
permission: '',
|
||||
sort: 0,
|
||||
hidden: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const title = computed(() => formData.id ? '编辑菜单' : '新增菜单')
|
||||
|
||||
const parentTreeData = computed(() => {
|
||||
return [{ id: 0, name: '顶级菜单', children: props.menuTree }]
|
||||
})
|
||||
|
||||
const formRules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入菜单编码', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择菜单类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// Reset to defaults
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
parentId: props.record?.parentId || 0, // Use record parentId if exists, else 0
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
permission: '',
|
||||
sort: 0,
|
||||
hidden: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// If full record provided (Edit mode), overwrite
|
||||
if (props.record && props.record.id) {
|
||||
Object.assign(formData, props.record)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function handleOk() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
if (formData.id) {
|
||||
await updateMenu(formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createMenu(formData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
111
src/components/system/role/RoleFormModal.vue
Normal file
111
src/components/system/role/RoleFormModal.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:title="title"
|
||||
:confirm-loading="loading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="角色编码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入角色编码(英文)" :disabled="!!formData.id" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入角色名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="formData.sort" :min="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">正常</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { createRole, updateRole } from '@/api/system/role'
|
||||
import type { RoleFormData } from '@/types/system/role'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
record?: RoleFormData
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive<RoleFormData>({
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
const title = computed(() => formData.id ? '编辑角色' : '新增角色')
|
||||
|
||||
const formRules: Record<string, Rule[]> = {
|
||||
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (props.record) {
|
||||
Object.assign(formData, props.record)
|
||||
} else {
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function handleOk() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
if (formData.id) {
|
||||
await updateRole(formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createRole(formData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
60
src/components/system/user/ResetPasswordModal.vue
Normal file
60
src/components/system/user/ResetPasswordModal.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="重置密码"
|
||||
:confirm-loading="loading"
|
||||
@ok="handleOk"
|
||||
@cancel="emit('update:visible', false)"
|
||||
>
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="新密码">
|
||||
<a-input-password v-model:value="newPassword" placeholder="请输入新密码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { resetUserPassword } from '@/api/system/user'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
userId?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
const newPassword = ref('')
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
newPassword.value = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function handleOk() {
|
||||
if (!newPassword.value) {
|
||||
message.warning('请输入新密码')
|
||||
return
|
||||
}
|
||||
if (!props.userId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await resetUserPassword(props.userId, newPassword.value)
|
||||
message.success('密码重置成功')
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('密码重置失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
144
src/components/system/user/UserFormModal.vue
Normal file
144
src/components/system/user/UserFormModal.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:title="title"
|
||||
:confirm-loading="loading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="formData.username" placeholder="请输入用户名" :disabled="!!formData.id" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="!formData.id" label="密码" name="password">
|
||||
<a-input-password v-model:value="formData.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="昵称" name="nickname">
|
||||
<a-input v-model:value="formData.nickname" placeholder="请输入昵称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色" name="role">
|
||||
<a-select v-model:value="formData.role" placeholder="请选择角色">
|
||||
<a-select-option v-for="role in roleOptions" :key="role.code" :value="role.code">
|
||||
{{ role.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">正常</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { createUser, updateUser } from '@/api/system/user'
|
||||
import type { UserFormData } from '@/types/system/user'
|
||||
|
||||
interface RoleOption {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
record?: UserFormData
|
||||
roleOptions: RoleOption[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive<UserFormData>({
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
role: undefined,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const title = computed(() => formData.id ? '编辑用户' : '新增用户')
|
||||
|
||||
const formRules: Record<string, Rule[]> = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (props.record) {
|
||||
Object.assign(formData, { ...props.record, password: '' })
|
||||
} else {
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
role: undefined,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function handleOk() {
|
||||
try {
|
||||
// Skip password validation if editing
|
||||
if (formData.id) {
|
||||
// A quick hack for optional password in edit mode if not handled by dynamic rules
|
||||
// AntDV form validation is powerful.
|
||||
// Ideally we should use dynamic rules.
|
||||
}
|
||||
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
if (formData.id) {
|
||||
await updateUser(formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createUser(formData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user