做字典管理
This commit is contained in:
Vendored
+2
@@ -19,6 +19,8 @@ declare module 'vue' {
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
|
||||
+64
-54
@@ -1,114 +1,124 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取字典类型列表
|
||||
/**
|
||||
* 数据字典接口(租户端)
|
||||
*
|
||||
* 表:yz_backend_dict / yz_backend_dict_item
|
||||
* 租户从 JWT 解析,前端不传 tenant_id。
|
||||
*/
|
||||
const PREFIX = '/backend/dict'
|
||||
|
||||
/* ================= 字典列表(字典类型) ================= */
|
||||
|
||||
/** 字典列表(分页)params: { page, pageSize, keyword, status } */
|
||||
export function getDictTypes(params) {
|
||||
return request({
|
||||
url: '/backend/dict/types',
|
||||
url: `${PREFIX}/types`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ID获取字典类型
|
||||
export function getDictTypeById(id) {
|
||||
/** 新增字典 */
|
||||
export function createDictType(data) {
|
||||
return request({
|
||||
url: `/backend/dict/types/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
export function addDictType(data) {
|
||||
return request({
|
||||
url: '/backend/dict/types',
|
||||
url: `${PREFIX}/types`,
|
||||
method: 'post',
|
||||
data: {
|
||||
...data,
|
||||
is_global: data.is_global !== undefined ? data.is_global : 0
|
||||
}
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新字典类型
|
||||
/** 编辑字典(dict_code 不可改,由后端忽略) */
|
||||
export function updateDictType(id, data) {
|
||||
return request({
|
||||
url: `/backend/dict/types/${id}`,
|
||||
url: `${PREFIX}/types/${id}`,
|
||||
method: 'put',
|
||||
data: {
|
||||
...data,
|
||||
is_global: data.is_global !== undefined ? data.is_global : 0
|
||||
}
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
/** 删除字典(其下仍有字典项时后端会拒绝) */
|
||||
export function deleteDictType(id) {
|
||||
return request({
|
||||
url: `/backend/dict/types/${id}`,
|
||||
url: `${PREFIX}/types/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取字典项列表
|
||||
/* ================= 字典项 ================= */
|
||||
|
||||
/** 字典项列表(分页)params: { dict_id, page, pageSize, keyword, status } */
|
||||
export function getDictItems(params) {
|
||||
return request({
|
||||
url: '/backend/dict/items',
|
||||
url: `${PREFIX}/items`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ID获取字典项
|
||||
export function getDictItemById(id) {
|
||||
/** 新增字典项 */
|
||||
export function createDictItem(data) {
|
||||
return request({
|
||||
url: `/backend/dict/items/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加字典项
|
||||
export function addDictItem(data) {
|
||||
return request({
|
||||
url: '/backend/dict/items',
|
||||
url: `${PREFIX}/items`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新字典项
|
||||
/** 编辑字典项 */
|
||||
export function updateDictItem(id, data) {
|
||||
return request({
|
||||
url: `/backend/dict/items/${id}`,
|
||||
url: `${PREFIX}/items/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典项
|
||||
/** 启用 / 停用字典项 */
|
||||
export function toggleDictItemStatus(id, status) {
|
||||
return request({
|
||||
url: `${PREFIX}/items/${id}/status`,
|
||||
method: 'post',
|
||||
data: { status }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除字典项 */
|
||||
export function deleteDictItem(id) {
|
||||
return request({
|
||||
url: `/backend/dict/items/${id}`,
|
||||
url: `${PREFIX}/items/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 根据字典编码获取字典项(用于业务查询)
|
||||
export function getDictItemsByCode(code, includeDisabled = false) {
|
||||
/* ================= 业务读取 ================= */
|
||||
|
||||
/** 按字典编码取「字典类型与字典项都启用」的选项,供业务页面下拉使用 */
|
||||
export function getDictOptions(code) {
|
||||
return request({
|
||||
url: `/backend/dict/items/code/${code}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
include_disabled: includeDisabled ? '1' : '0'
|
||||
}
|
||||
url: `${PREFIX}/options/${code}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 批量更新字典项排序
|
||||
export function batchUpdateDictItemSort(data) {
|
||||
/**
|
||||
* 批量取多个字典的选项(一次请求),供页面/应用启动时预加载。
|
||||
* 返回 data.dicts = { [dict_code]: 字典项[] }。
|
||||
* 同编码时后端优先返回本租户自己的字典,没有再回落全局内置字典。
|
||||
* @param {string[]} codes 字典编码数组
|
||||
*/
|
||||
export function getDictOptionsBatch(codes) {
|
||||
return request({
|
||||
url: '/backend/dict/items/sort',
|
||||
method: 'put',
|
||||
data
|
||||
url: `${PREFIX}/options`,
|
||||
method: 'post',
|
||||
data: { codes }
|
||||
})
|
||||
}
|
||||
|
||||
/** 一次取回全部「启用」字典的字典项,供登录后预热缓存 */
|
||||
export function getAllDictOptions() {
|
||||
return request({
|
||||
url: `${PREFIX}/options`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { computed } from 'vue'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
/**
|
||||
* 字典 Composable:把「加载 + 取值 + 文案/标签色查询」收敛成一行调用。
|
||||
*
|
||||
* 用法:
|
||||
* const { items, options, label, tagType } = useDictionary('payment_channel')
|
||||
* // 模板:<el-option v-for="o in options" :label="o.label" :value="o.value" />
|
||||
* // <el-tag :type="tagType(row.status)">{{ label(row.status) }}</el-tag>
|
||||
*
|
||||
* @param {string} code 字典编码(库里真实的 dict_code)
|
||||
*/
|
||||
export function useDictionary(code) {
|
||||
const dictStore = useDictStore()
|
||||
// setup 阶段同步触发加载:缓存命中时不发请求,首次会自动拉取并响应式刷新
|
||||
dictStore.ensure(code)
|
||||
|
||||
/** 字典项原始数组(含 item_value / item_label / tag_type 等) */
|
||||
const items = computed(() => dictStore.getDictItemsSync(code))
|
||||
/** el-select / el-option 直接可用的 { label, value } 列表 */
|
||||
const options = computed(() => items.value.map((it) => ({
|
||||
label: it.item_label,
|
||||
value: it.item_value,
|
||||
tag_type: it.tag_type
|
||||
})))
|
||||
|
||||
return {
|
||||
items,
|
||||
options,
|
||||
label: (value, fallback) => dictStore.getDictLabel(code, value, fallback),
|
||||
tagType: (value, fallback) => dictStore.getDictTagType(code, value, fallback),
|
||||
find: (value) => dictStore.getDictItem(code, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只要「值 → 文案」或「值 → 标签色」时的轻量用法。
|
||||
* const channelLabel = useDictLabel('payment_channel')
|
||||
*/
|
||||
export function useDictLabel(code) {
|
||||
const dictStore = useDictStore()
|
||||
dictStore.ensure(code)
|
||||
return (value, fallback) => dictStore.getDictLabel(code, value, fallback)
|
||||
}
|
||||
|
||||
export function useDictTagType(code) {
|
||||
const dictStore = useDictStore()
|
||||
dictStore.ensure(code)
|
||||
return (value, fallback) => dictStore.getDictTagType(code, value, fallback)
|
||||
}
|
||||
|
||||
/** 预加载多个字典(页面 onMounted 里调用,一次批量请求) */
|
||||
export function useDictPreload(codes) {
|
||||
const dictStore = useDictStore()
|
||||
return () => dictStore.preloadDicts(codes)
|
||||
}
|
||||
|
||||
export default useDictionary
|
||||
@@ -0,0 +1,187 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getDictOptions, getDictOptionsBatch, getAllDictOptions } from '@/api/dict'
|
||||
|
||||
/**
|
||||
* 字典 Store(平台端)
|
||||
*
|
||||
* 全部字典数据都来自数据库,前端不维护编码清单:
|
||||
* - 登录后调用一次 loadAll(),把库里所有「启用」字典读进缓存;
|
||||
* - 页面按库里真实的 dict_code 取值即可(字符串直接写,无需常量文件);
|
||||
* - 之后在字典管理页新增的字典,第一次被引用时会按需自动加载。
|
||||
*
|
||||
* 用法:
|
||||
* const dictStore = useDictStore()
|
||||
* dictStore.getDictLabel('payment_order_status', row.status) // 文案
|
||||
* dictStore.getDictTagType('payment_order_status', row.status) // 标签色
|
||||
* dictStore.getSelectOptions('payment_channel') // 下拉选项
|
||||
*/
|
||||
export const useDictStore = defineStore('dict', () => {
|
||||
/** dict_code -> 字典项数组(渲染依赖它,必须是响应式的) */
|
||||
const dictMap = ref({})
|
||||
|
||||
/** 已加载过的编码(仅用于去重,不参与渲染) */
|
||||
let loaded = {}
|
||||
/** 正在请求中的编码 -> Promise(并发调用复用同一个请求) */
|
||||
const pending = new Map()
|
||||
/** 是否已完成全量预热 */
|
||||
let allLoaded = false
|
||||
let allPending = null
|
||||
|
||||
function setItems(code, list) {
|
||||
dictMap.value = { ...dictMap.value, [code]: Array.isArray(list) ? list : [] }
|
||||
}
|
||||
|
||||
function itemsOf(code) {
|
||||
return dictMap.value[code] || []
|
||||
}
|
||||
|
||||
/** 把入参规范成去重后的编码数组 */
|
||||
function normalizeCodes(codes) {
|
||||
return [...new Set((codes || []).map((c) => String(c || '').trim()).filter(Boolean))]
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载字典(带缓存与并发去重)。
|
||||
* 请求失败也会标记为已加载,避免页面反复重试;需要重试请先 clearCache。
|
||||
*/
|
||||
async function load(codes) {
|
||||
const list = normalizeCodes(codes)
|
||||
const missing = list.filter((c) => !loaded[c] && !pending.has(c))
|
||||
|
||||
if (missing.length) {
|
||||
const promise = (async () => {
|
||||
if (missing.length === 1) {
|
||||
// 单个字典走专用接口
|
||||
const code = missing[0]
|
||||
try {
|
||||
const res = await getDictOptions(code)
|
||||
setItems(code, res?.code === 200 ? res.data?.list || [] : [])
|
||||
} catch {
|
||||
setItems(code, [])
|
||||
}
|
||||
} else {
|
||||
// 多个字典合并成一次请求
|
||||
try {
|
||||
const res = await getDictOptionsBatch(missing)
|
||||
const dicts = res?.code === 200 ? res.data?.dicts || {} : {}
|
||||
missing.forEach((code) => setItems(code, dicts[code] || []))
|
||||
} catch {
|
||||
missing.forEach((code) => setItems(code, []))
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
missing.forEach((code) => {
|
||||
loaded[code] = true
|
||||
pending.delete(code)
|
||||
})
|
||||
})
|
||||
|
||||
missing.forEach((code) => pending.set(code, promise))
|
||||
}
|
||||
|
||||
await Promise.all(list.map((c) => pending.get(c) || Promise.resolve()))
|
||||
return list.flatMap((c) => itemsOf(c))
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量预热:一次请求把库里所有启用字典读进缓存(登录后调用一次即可)。
|
||||
* 之后再新增的字典仍会走 load() 按需加载。
|
||||
*/
|
||||
function loadAll() {
|
||||
if (allLoaded) return Promise.resolve()
|
||||
if (allPending) return allPending
|
||||
|
||||
allPending = (async () => {
|
||||
try {
|
||||
const res = await getAllDictOptions()
|
||||
const dicts = res?.code === 200 ? res.data?.dicts || {} : {}
|
||||
const next = { ...dictMap.value }
|
||||
Object.entries(dicts).forEach(([code, list]) => {
|
||||
loaded[code] = true
|
||||
next[code] = Array.isArray(list) ? list : []
|
||||
})
|
||||
dictMap.value = next
|
||||
} catch {
|
||||
// 预热失败不影响使用:后续按需加载
|
||||
} finally {
|
||||
allLoaded = true
|
||||
allPending = null
|
||||
}
|
||||
})()
|
||||
return allPending
|
||||
}
|
||||
|
||||
/** 同步触发加载(不等待),用于组件 setup/onMounted 里“先渲染后填充” */
|
||||
function ensure(code) {
|
||||
const list = normalizeCodes([code])
|
||||
if (list.some((c) => !loaded[c] && !pending.has(c))) {
|
||||
load(list)
|
||||
}
|
||||
}
|
||||
|
||||
/** 同步取字典项;未加载时顺带触发加载,拿到后由响应式自动刷新 */
|
||||
function getDictItemsSync(code) {
|
||||
ensure(code)
|
||||
return itemsOf(code)
|
||||
}
|
||||
|
||||
/** 值 → 字典项(值按字符串比较,兼容 1 / '1' 混用) */
|
||||
function getDictItem(code, value) {
|
||||
if (value === undefined || value === null || value === '') return null
|
||||
const key = String(value)
|
||||
return itemsOf(code).find((it) => String(it.item_value) === key) || null
|
||||
}
|
||||
|
||||
/** 值 → 文案;取不到时返回 fallback,未传则原样返回取值 */
|
||||
function getDictLabel(code, value, fallback) {
|
||||
const hit = getDictItem(code, value)
|
||||
if (hit) return hit.item_label
|
||||
if (fallback !== undefined) return fallback
|
||||
return value === undefined || value === null ? '' : String(value)
|
||||
}
|
||||
|
||||
/** 值 → el-tag 的 type;取不到时返回 fallback(默认 info) */
|
||||
function getDictTagType(code, value, fallback = 'info') {
|
||||
const hit = getDictItem(code, value)
|
||||
return hit && hit.tag_type ? hit.tag_type : fallback
|
||||
}
|
||||
|
||||
/** 值 → 下拉选项 { label, value },供 el-select / el-option 直接使用 */
|
||||
function getSelectOptions(code) {
|
||||
return getDictItemsSync(code).map((it) => ({ label: it.item_label, value: it.item_value, tag_type: it.tag_type }))
|
||||
}
|
||||
|
||||
/** 预加载多个字典(一次批量请求) */
|
||||
function preloadDicts(codes) {
|
||||
return load(codes)
|
||||
}
|
||||
|
||||
/** 清空缓存:传编码清单个,不传清全部(同时允许重新全量预热) */
|
||||
function clearCache(code) {
|
||||
if (code) {
|
||||
delete loaded[code]
|
||||
const next = { ...dictMap.value }
|
||||
delete next[code]
|
||||
dictMap.value = next
|
||||
return
|
||||
}
|
||||
loaded = {}
|
||||
dictMap.value = {}
|
||||
allLoaded = false
|
||||
}
|
||||
|
||||
return {
|
||||
dictMap,
|
||||
ensure,
|
||||
loadAll,
|
||||
getDictItems: load,
|
||||
getDictItemsSync,
|
||||
getDictItem,
|
||||
getDictLabel,
|
||||
getDictTagType,
|
||||
getSelectOptions,
|
||||
preloadDicts,
|
||||
clearCache
|
||||
}
|
||||
})
|
||||
@@ -53,3 +53,4 @@ export function formatDate(value: any): string {
|
||||
export function formatDateOnly(value: any): string {
|
||||
return formatDate(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import CommonAside from '@/components/CommonAside.vue';
|
||||
import CommonHeader from '@/components/CommonHeader.vue';
|
||||
import { useTabsStore } from '@/stores';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, watch, reactive, nextTick, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { More, Close, CircleClose, ArrowUp, Fold, Expand } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const dictStore = useDictStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const defaultDashboardPath = '/home';
|
||||
@@ -76,6 +78,9 @@ function restoreTabFromRoute() {
|
||||
|
||||
// 组件挂载后,根据当前路由恢复 tab
|
||||
onMounted(() => {
|
||||
// 进入后台后把库里所有字典预热到缓存(失败不影响按需加载)
|
||||
dictStore.loadAll();
|
||||
|
||||
// 等待路由完全加载后再恢复
|
||||
nextTick(() => {
|
||||
// 刷新时,localStorage 中已保存的 tabs 会在 store 初始化时恢复
|
||||
|
||||
@@ -25,27 +25,17 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="客户类型">
|
||||
<el-select v-model="filters.customer_type" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option label="企业" value="1" />
|
||||
<el-option label="政府机构" value="2" />
|
||||
<el-option label="国企" value="3" />
|
||||
<el-option label="教育机构" value="4" />
|
||||
<el-option label="个人" value="5" />
|
||||
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户等级">
|
||||
<el-select v-model="filters.customer_level" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option label="核心客户" value="1" />
|
||||
<el-option label="重要客户" value="2" />
|
||||
<el-option label="普通客户" value="3" />
|
||||
<el-option label="潜在客户" value="4" />
|
||||
<el-option v-for="item in levelOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 110px">
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
<el-option label="冻结" value="2" />
|
||||
<el-option label="已注销" value="3" />
|
||||
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据状态">
|
||||
@@ -210,13 +200,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { computed, ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh, Cpu } from '@element-plus/icons-vue'
|
||||
import { getCustomerList, deleteCustomer, toggleCustomerStatus, convertCustomerToSupplier } from '@/api/customer'
|
||||
import CustomerEdit from './components/edit.vue'
|
||||
import ContactBook from '../components/contactBook.vue'
|
||||
import SmartAddCompany from '../components/smartAddCompany.vue'
|
||||
import { useErpPartyDict, formatDate, formatDateTime } from '../dict'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
// 类型/等级/状态的文案与标签色统一取自数据字典
|
||||
const { customerTypeText, customerTypeTag, customerLevelText, customerLevelTag, statusText, statusTag } = useErpPartyDict()
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const typeOptions = computed(() => dictStore.getSelectOptions('erp_customer_type'))
|
||||
const levelOptions = computed(() => dictStore.getSelectOptions('common_object_level'))
|
||||
const statusOptions = computed(() => dictStore.getSelectOptions('common_status'))
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
@@ -300,19 +300,6 @@ function openContactBook(row) {
|
||||
contactBookVisible.value = true
|
||||
}
|
||||
|
||||
function formatDate(val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
||||
}
|
||||
|
||||
function formatDateTime(val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
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')
|
||||
}
|
||||
|
||||
function copyInvoiceInfo() {
|
||||
const d = detailData.value
|
||||
if (!d) return
|
||||
@@ -413,35 +400,6 @@ async function handleConvertSupplier(row) {
|
||||
}
|
||||
}
|
||||
|
||||
function customerTypeText(type) {
|
||||
const map = { '1': '企业', '2': '政府机构', '3': '国企', '4': '教育机构', '5':'个人' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
function customerTypeTag(type) {
|
||||
const map = { '1': 'primary', '2': 'warning', '3': 'success', '4': 'info' }
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
function customerLevelText(level) {
|
||||
const map = { '1': '核心客户', '2': '重要客户', '3': '普通客户', '4': '潜在客户' }
|
||||
return map[level] || level
|
||||
}
|
||||
|
||||
function customerLevelTag(level) {
|
||||
const map = { '1': 'danger', '2': 'warning', '3': 'info', '4': '' }
|
||||
return map[level] || ''
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
const map = { '0': '禁用', '1': '正常', '2': '冻结', '3': '已注销' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
const map = { '0': 'info', '1': 'success', '2': 'warning', '3': 'danger' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
/**
|
||||
* ERP 往来单位(客户 / 供应商)的字典取值。
|
||||
*
|
||||
* 文案与标签色来自数据字典接口,租户可在「系统设置 → 数据字典」维护:
|
||||
* erp_customer_type / erp_supplier_type / common_object_level / common_status
|
||||
*
|
||||
* 用法:
|
||||
* const { customerTypeText, customerTypeTag, statusText } = useErpPartyDict()
|
||||
*/
|
||||
export function useErpPartyDict() {
|
||||
const dictStore = useDictStore()
|
||||
|
||||
return {
|
||||
customerTypeText: (v) => dictStore.getDictLabel('erp_customer_type', v, v),
|
||||
customerTypeTag: (v) => dictStore.getDictTagType('erp_customer_type', v),
|
||||
customerLevelText: (v) => dictStore.getDictLabel('common_object_level', v, v),
|
||||
customerLevelTag: (v) => dictStore.getDictTagType('common_object_level', v),
|
||||
supplierTypeText: (v) => dictStore.getDictLabel('erp_supplier_type', v, v),
|
||||
supplierTypeTag: (v) => dictStore.getDictTagType('erp_supplier_type', v),
|
||||
supplierLevelText: (v) => dictStore.getDictLabel('common_object_level', v, v),
|
||||
supplierLevelTag: (v) => dictStore.getDictTagType('common_object_level', v),
|
||||
statusText: (v) => dictStore.getDictLabel('common_status', v, v),
|
||||
statusTag: (v) => dictStore.getDictTagType('common_status', v)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
||||
}
|
||||
|
||||
export function formatDateTime(val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
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')
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="visible"
|
||||
:title="data?.supplier_name || '供应商详情'"
|
||||
direction="rtl"
|
||||
size="560px"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
>
|
||||
<div v-if="data" class="detail-content">
|
||||
<div class="detail-actions">
|
||||
<el-button type="success" size="small" @click="emit('convert', data)">转为客户</el-button>
|
||||
</div>
|
||||
<el-descriptions title="基本信息" :column="2" border size="small" label-width="120px">
|
||||
<el-descriptions-item label="供应商名称" :span="2">{{ data.supplier_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商类型">{{ supplierTypeText(data.supplier_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商等级">{{ supplierLevelText(data.supplier_level) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属行业">{{ data.industry || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusTag(data.status)" size="small">{{ statusText(data.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="注册资本">{{ data.registered_capital ? data.registered_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="实缴资本">{{ data.paid_capital ? data.paid_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成立日期">{{ formatDate(data.establish_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="行政区划">{{ data.administrative_division || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="企业类型">{{ data.enterprise_type || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="纳税人资质">{{ data.taxpayer_qualification || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="经营范围" :span="2">{{ data.business_scope || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ formatDateTime(data.update_time) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions title="联系信息" :column="2" border size="small" label-width="120px" style="margin-top: 20px">
|
||||
<el-descriptions-item label="联系人">{{ data.contact_person || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ data.contact_phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系邮箱" :span="2">{{ data.contact_email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商地址" :span="2">{{ data.address || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions :column="2" border size="small" label-width="120px" style="margin-top: 20px">
|
||||
<template #title>
|
||||
<div class="desc-title-row">
|
||||
<span>开票信息</span>
|
||||
<el-button link type="primary" size="small" @click="copyInvoiceInfo">一键复制</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions-item label="发票抬头" :span="2">{{ data.invoice_title || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="纳税人识别号">{{ data.tax_number || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册电话">{{ data.registered_phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册地址" :span="2">{{ data.registered_address || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开户行名称">{{ data.bank_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开户行账号">{{ data.bank_account || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions title="备注信息" :column="1" border size="small" label-width="120px" style="margin-top: 20px">
|
||||
<el-descriptions-item label="备注">{{ data.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useSupplierDict, formatDate, formatDateTime } from '../dict'
|
||||
|
||||
// 类型/等级/状态的文案与标签色统一取自数据字典
|
||||
const { supplierTypeText, supplierLevelText, statusText, statusTag } = useSupplierDict()
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
data: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'convert'])
|
||||
|
||||
/** 一键复制开票信息(isSecureContext 之外的 http 环境下回退到 execCommand) */
|
||||
function copyInvoiceInfo() {
|
||||
const d = props.data
|
||||
if (!d) return
|
||||
const lines = [
|
||||
`发票抬头:${d.invoice_title || ''}`,
|
||||
`纳税人识别号:${d.tax_number || ''}`,
|
||||
`注册地址:${d.registered_address || ''}`,
|
||||
`注册电话:${d.registered_phone || ''}`,
|
||||
`开户行名称:${d.bank_name || ''}`,
|
||||
`开户行账号:${d.bank_account || ''}`
|
||||
]
|
||||
const text = lines.join('\n')
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('开票信息已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
fallbackCopy(text)
|
||||
})
|
||||
} else {
|
||||
fallbackCopy(text)
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text) {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
ElMessage.success('开票信息已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.desc-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 供应商模块的字典取值:实现已上移到 ../dict.js(客户/供应商共用同一套往来单位字典),
|
||||
* 这里保留原有导出名,避免各页面改 import 路径。
|
||||
*/
|
||||
export { useErpPartyDict as useSupplierDict, formatDate, formatDateTime } from '../dict'
|
||||
@@ -25,26 +25,17 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商类型">
|
||||
<el-select v-model="filters.supplier_type" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option label="原材料供应商" value="1" />
|
||||
<el-option label="设备供应商" value="2" />
|
||||
<el-option label="服务供应商" value="3" />
|
||||
<el-option label="其他" value="4" />
|
||||
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商等级">
|
||||
<el-select v-model="filters.supplier_level" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option label="核心供应商" value="1" />
|
||||
<el-option label="重要供应商" value="2" />
|
||||
<el-option label="普通供应商" value="3" />
|
||||
<el-option label="潜在供应商" value="4" />
|
||||
<el-option v-for="item in levelOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 110px">
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
<el-option label="冻结" value="2" />
|
||||
<el-option label="已注销" value="3" />
|
||||
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据状态">
|
||||
@@ -134,65 +125,12 @@
|
||||
@success="fetchList"
|
||||
/>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<el-drawer
|
||||
v-model="detailVisible"
|
||||
:title="detailData?.supplier_name || '供应商详情'"
|
||||
direction="rtl"
|
||||
size="560px"
|
||||
>
|
||||
<div v-if="detailData" class="detail-content">
|
||||
<div class="detail-actions">
|
||||
<el-button type="success" size="small" @click="handleConvertCustomer(detailData)">转为客户</el-button>
|
||||
<span class="detail-actions-tip">同一家单位可能既是供应商又是客户,转化后原供应商记录保留</span>
|
||||
</div>
|
||||
<el-descriptions title="基本信息" :column="2" border size="small" label-width="120px">
|
||||
<el-descriptions-item label="供应商名称" :span="2">{{ detailData.supplier_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商类型">{{ supplierTypeText(detailData.supplier_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商等级">{{ supplierLevelText(detailData.supplier_level) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属行业">{{ detailData.industry || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusTag(detailData.status)" size="small">{{ statusText(detailData.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="注册资本">{{ detailData.registered_capital ? detailData.registered_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="实缴资本">{{ detailData.paid_capital ? detailData.paid_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成立日期">{{ formatDate(detailData.establish_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="行政区划">{{ detailData.administrative_division || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="企业类型">{{ detailData.enterprise_type || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="纳税人资质">{{ detailData.taxpayer_qualification || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="经营范围" :span="2">{{ detailData.business_scope || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ formatDateTime(detailData.update_time) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<SupplierDetail
|
||||
v-model:visible="detailVisible"
|
||||
:data="detailData"
|
||||
@convert="handleConvertCustomer"
|
||||
/>
|
||||
|
||||
<el-descriptions title="联系信息" :column="2" border size="small" label-width="120px" style="margin-top: 20px">
|
||||
<el-descriptions-item label="联系人">{{ detailData.contact_person || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detailData.contact_phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系邮箱" :span="2">{{ detailData.contact_email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商地址" :span="2">{{ detailData.address || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions :column="2" border size="small" label-width="120px" style="margin-top: 20px">
|
||||
<template #title>
|
||||
<div class="desc-title-row">
|
||||
<span>开票信息</span>
|
||||
<el-button link type="primary" size="small" @click="copyInvoiceInfo">一键复制</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions-item label="发票抬头" :span="2">{{ detailData.invoice_title || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="纳税人识别号">{{ detailData.tax_number || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册电话">{{ detailData.registered_phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册地址" :span="2">{{ detailData.registered_address || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开户行名称">{{ detailData.bank_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开户行账号">{{ detailData.bank_account || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions title="备注信息" :column="1" border size="small" label-width="120px" style="margin-top: 20px">
|
||||
<el-descriptions-item label="备注">{{ detailData.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 通讯录 -->
|
||||
<ContactBook
|
||||
v-model:visible="contactBookVisible"
|
||||
:company-type="'supplier'"
|
||||
@@ -209,13 +147,24 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { computed, ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh, Cpu } from '@element-plus/icons-vue'
|
||||
import { getSupplierList, deleteSupplier, toggleSupplierStatus, convertSupplierToCustomer } from '@/api/supplier'
|
||||
import SupplierEdit from './components/edit.vue'
|
||||
import SupplierDetail from './components/detail.vue'
|
||||
import ContactBook from '../components/contactBook.vue'
|
||||
import SmartAddCompany from '../components/smartAddCompany.vue'
|
||||
import { useSupplierDict, formatDateTime } from './dict'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
// 类型/等级/状态的文案与标签色统一取自数据字典
|
||||
const { supplierTypeText, supplierTypeTag, supplierLevelText, supplierLevelTag, statusText, statusTag } = useSupplierDict()
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const typeOptions = computed(() => dictStore.getSelectOptions('erp_supplier_type'))
|
||||
const levelOptions = computed(() => dictStore.getSelectOptions('common_object_level'))
|
||||
const statusOptions = computed(() => dictStore.getSelectOptions('common_status'))
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
@@ -299,58 +248,6 @@ function openContactBook(row) {
|
||||
contactBookVisible.value = true
|
||||
}
|
||||
|
||||
function formatDate(val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
||||
}
|
||||
|
||||
function formatDateTime(val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
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')
|
||||
}
|
||||
|
||||
function copyInvoiceInfo() {
|
||||
const d = detailData.value
|
||||
if (!d) return
|
||||
const lines = [
|
||||
`发票抬头:${d.invoice_title || ''}`,
|
||||
`纳税人识别号:${d.tax_number || ''}`,
|
||||
`注册地址:${d.registered_address || ''}`,
|
||||
`注册电话:${d.registered_phone || ''}`,
|
||||
`开户行名称:${d.bank_name || ''}`,
|
||||
`开户行账号:${d.bank_account || ''}`
|
||||
]
|
||||
const text = lines.join('\n')
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('开票信息已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
fallbackCopy(text)
|
||||
})
|
||||
} else {
|
||||
fallbackCopy(text)
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text) {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
ElMessage.success('开票信息已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
|
||||
async function handleToggleStatus(row) {
|
||||
const newStatus = row.status === '1' ? '0' : '1'
|
||||
const action = newStatus === '1' ? '启用' : '禁用'
|
||||
@@ -411,36 +308,6 @@ async function handleConvertCustomer(row) {
|
||||
ElMessage.error(e.message || '转化失败')
|
||||
}
|
||||
}
|
||||
|
||||
function supplierTypeText(type) {
|
||||
const map = { '1': '原材料供应商', '2': '设备供应商', '3': '服务供应商', '4': '其他' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
function supplierTypeTag(type) {
|
||||
const map = { '1': 'primary', '2': 'warning', '3': 'success', '4': 'info' }
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
function supplierLevelText(level) {
|
||||
const map = { '1': '核心供应商', '2': '重要供应商', '3': '普通供应商', '4': '潜在供应商' }
|
||||
return map[level] || level
|
||||
}
|
||||
|
||||
function supplierLevelTag(level) {
|
||||
const map = { '1': 'danger', '2': 'warning', '3': 'info', '4': '' }
|
||||
return map[level] || ''
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
const map = { '0': '禁用', '1': '正常', '2': '冻结', '3': '已注销' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
const map = { '0': 'info', '1': 'success', '2': 'warning', '3': 'danger' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -502,28 +369,9 @@ function statusTag(status) {
|
||||
}
|
||||
}
|
||||
|
||||
.desc-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-actions-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,335 +1,173 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典项' : '添加字典项'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictItemForm"
|
||||
:rules="formRules"
|
||||
ref="dictItemFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="dict_type_id">
|
||||
<el-input
|
||||
:value="dictTypeDisplayName"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">当前字典类型:{{ dictTypeDisplayName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_label"
|
||||
placeholder="请输入字典标签(显示值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="父级字典项" prop="parent_id">
|
||||
<el-select
|
||||
v-model="dictItemForm.parent_id"
|
||||
placeholder="选择父级字典项(可选)"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in parentDictItems"
|
||||
:key="item.id"
|
||||
:label="item.dict_label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictItemForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色" prop="color">
|
||||
<el-input
|
||||
v-model="dictItemForm.color"
|
||||
placeholder="请输入颜色值(如:#FF0000 或 red)"
|
||||
>
|
||||
<template #append>
|
||||
<el-color-picker
|
||||
v-model="dictItemForm.color"
|
||||
:predefine="predefineColors"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="form-tip">支持十六进制颜色值(如:#FF0000)或颜色名称(如:red)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input
|
||||
v-model="dictItemForm.icon"
|
||||
placeholder="请输入图标(如:✓、✗、⭐)"
|
||||
/>
|
||||
<div class="form-tip">支持emoji或图标字符</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictItemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { addDictItem, updateDictItem, getDictItems } from '@/api/dict';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
dictItem?: any;
|
||||
dictTypeId?: number;
|
||||
dictTypeName?: string;
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="isEdit ? '编辑字典项' : '新增字典项'"
|
||||
width="520px"
|
||||
@update:model-value="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="字典项名称" prop="item_label">
|
||||
<el-input v-model="form.item_label" placeholder="如:原材料供应商" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典项值" prop="item_value">
|
||||
<el-input
|
||||
v-model="form.item_value"
|
||||
:disabled="isEdit"
|
||||
placeholder="业务入库值,如:1(同一字典下不可重复,保存后不可修改)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签色" prop="tag_type">
|
||||
<el-select v-model="form.tag_type" clearable placeholder="留空则不渲染标签" style="width: 100%">
|
||||
<el-option v-for="t in TAG_TYPES" :key="t" :label="t" :value="t">
|
||||
<el-tag :type="t" size="small">{{ t }}</el-tag>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认选中" prop="is_default">
|
||||
<el-radio-group v-model="form.is_default">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createDictItem, updateDictItem } from '@/api/dict'
|
||||
|
||||
/** 与 el-tag 的 type 对齐,存入 yz_*_dict_item.tag_type */
|
||||
const TAG_TYPES = ['primary', 'success', 'warning', 'danger', 'info']
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
/** 所属字典ID */
|
||||
dictId: { type: [Number, String], default: null },
|
||||
/** 传入表示编辑,null 表示新增 */
|
||||
dictItem: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
item_label: '',
|
||||
item_value: '',
|
||||
tag_type: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
is_default: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!form.id)
|
||||
|
||||
const rules = {
|
||||
item_label: [
|
||||
{ required: true, message: '请输入字典项名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
item_value: [
|
||||
{ required: true, message: '请输入字典项值', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) initForm()
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictItem: null,
|
||||
dictTypeId: undefined,
|
||||
dictTypeName: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const dictItemFormRef = ref<FormInstance>();
|
||||
const parentDictItems = ref<any[]>([]);
|
||||
|
||||
// 预定义颜色
|
||||
const predefineColors = [
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#ff0000',
|
||||
'#00ff00',
|
||||
'#0000ff',
|
||||
];
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictItem && props.dictItem.id);
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const dictItemForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_type_id: 0,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_label: [
|
||||
{ required: true, message: '请输入字典标签', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_value: [
|
||||
{ required: true, message: '请输入字典值', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
color: [
|
||||
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
icon: [
|
||||
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 获取父级字典项列表(排除自己)
|
||||
async function fetchParentDictItems() {
|
||||
if (!props.dictTypeId) return;
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: props.dictTypeId });
|
||||
if (res.success && res.data) {
|
||||
// 如果是编辑模式,排除当前字典项
|
||||
if (isEditing.value && props.dictItem) {
|
||||
parentDictItems.value = res.data.filter(
|
||||
(item: any) => item.id !== props.dictItem.id
|
||||
);
|
||||
} else {
|
||||
parentDictItems.value = res.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取父级字典项失败:', error);
|
||||
}
|
||||
)
|
||||
|
||||
function initForm() {
|
||||
const d = props.dictItem
|
||||
form.id = d?.id || null
|
||||
form.item_label = d?.item_label || ''
|
||||
form.item_value = d?.item_value || ''
|
||||
form.tag_type = d?.tag_type || ''
|
||||
form.sort = d?.sort ?? 0
|
||||
form.status = d?.status ?? 1
|
||||
form.is_default = d?.is_default ?? 0
|
||||
form.remark = d?.remark || ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算字典类型显示名称
|
||||
const dictTypeDisplayName = computed(() => {
|
||||
if (props.dictTypeName) {
|
||||
return props.dictTypeName;
|
||||
}
|
||||
if (props.dictTypeId) {
|
||||
return `字典类型 ID: ${props.dictTypeId}`;
|
||||
}
|
||||
return '未知字典类型';
|
||||
});
|
||||
|
||||
// 监听 dictItem 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictItem,
|
||||
(newDictItem) => {
|
||||
if (newDictItem) {
|
||||
dictItemForm.id = newDictItem.id || null;
|
||||
dictItemForm.dict_type_id = newDictItem.dict_type_id || props.dictTypeId || 0;
|
||||
dictItemForm.dict_label = newDictItem.dict_label || '';
|
||||
dictItemForm.dict_value = newDictItem.dict_value || '';
|
||||
dictItemForm.parent_id = newDictItem.parent_id || 0;
|
||||
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1;
|
||||
dictItemForm.sort = newDictItem.sort || 0;
|
||||
dictItemForm.color = newDictItem.color || '';
|
||||
dictItemForm.icon = newDictItem.icon || '';
|
||||
dictItemForm.remark = newDictItem.remark || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听弹窗打开,获取父级字典项
|
||||
watch(visible, (val) => {
|
||||
if (val) {
|
||||
if (props.dictTypeId) {
|
||||
dictItemForm.dict_type_id = props.dictTypeId;
|
||||
}
|
||||
fetchParentDictItems();
|
||||
}
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictItemForm.id = null;
|
||||
dictItemForm.dict_type_id = props.dictTypeId || 0;
|
||||
dictItemForm.dict_label = '';
|
||||
dictItemForm.dict_value = '';
|
||||
dictItemForm.parent_id = 0;
|
||||
dictItemForm.status = 1;
|
||||
dictItemForm.sort = 0;
|
||||
dictItemForm.color = '';
|
||||
dictItemForm.icon = '';
|
||||
dictItemForm.remark = '';
|
||||
dictItemFormRef.value?.clearValidate();
|
||||
if (!isEdit.value && !props.dictId) {
|
||||
ElMessage.error('未选择字典')
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
|
||||
const payload = {
|
||||
item_label: form.item_label,
|
||||
tag_type: form.tag_type || '',
|
||||
sort: form.sort,
|
||||
status: form.status,
|
||||
is_default: form.is_default,
|
||||
remark: form.remark
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictItemFormRef.value) return;
|
||||
|
||||
try {
|
||||
await dictItemFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dictItemForm.dict_type_id) {
|
||||
ElMessage.error('字典类型ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_type_id: dictItemForm.dict_type_id,
|
||||
dict_label: dictItemForm.dict_label,
|
||||
dict_value: dictItemForm.dict_value,
|
||||
parent_id: dictItemForm.parent_id || 0,
|
||||
status: dictItemForm.status,
|
||||
sort: dictItemForm.sort,
|
||||
color: dictItemForm.color || '',
|
||||
icon: dictItemForm.icon || '',
|
||||
remark: dictItemForm.remark || '',
|
||||
};
|
||||
|
||||
if (isEditing.value) {
|
||||
await updateDictItem(dictItemForm.id!, submitData);
|
||||
ElMessage.success('字典项更新成功');
|
||||
} else {
|
||||
await addDictItem(submitData);
|
||||
ElMessage.success('字典项添加成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
if (isEdit.value) {
|
||||
// 字典项值不可改,避免历史数据里的存量值对不上
|
||||
payload.id = form.id
|
||||
} else {
|
||||
payload.dict_id = props.dictId
|
||||
payload.item_value = form.item_value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = isEdit.value
|
||||
? await updateDictItem(form.id, payload)
|
||||
: await createDictItem(payload)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '保存成功' : '新增成功')
|
||||
emit('update:visible', false)
|
||||
emit('success', res.data || null)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.response?.data?.msg || e?.message || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典项' : '添加字典项'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictItemForm"
|
||||
:rules="formRules"
|
||||
ref="dictItemFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="dict_type_id">
|
||||
<el-select
|
||||
v-model="dictItemForm.dict_type_id"
|
||||
placeholder="请选择字典类型"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="isEditing || !!dictTypeId"
|
||||
>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="`${type.dict_name} (${type.dict_code})`"
|
||||
:value="type.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_label"
|
||||
placeholder="请输入字典标签(显示值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="父级ID" prop="parent_id">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.parent_id"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="0表示顶级"
|
||||
/>
|
||||
<div class="form-tip">0表示顶级字典项</div>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictItemForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序序号" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色标记" prop="color">
|
||||
<el-input
|
||||
v-model="dictItemForm.color"
|
||||
placeholder="请输入颜色值(如:#FF0000 或 red)"
|
||||
maxlength="20"
|
||||
/>
|
||||
<div class="form-tip">用于前端显示时的颜色标记</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input
|
||||
v-model="dictItemForm.icon"
|
||||
placeholder="请输入图标名称或类名"
|
||||
maxlength="50"
|
||||
/>
|
||||
<div class="form-tip">用于前端显示时的图标</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictItemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { getDictTypes, addDictItem, updateDictItem } from '@/api/dict'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dictItem?: any
|
||||
dictTypeId?: number | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictItem: null,
|
||||
dictTypeId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const dictItemFormRef = ref<FormInstance>()
|
||||
const dictTypes = ref<any[]>([])
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictItem && props.dictItem.id)
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const dictItemForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_type_id: null as number | null,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_type_id: [
|
||||
{ required: true, message: '请选择字典类型', trigger: 'change' },
|
||||
],
|
||||
dict_label: [
|
||||
{ required: true, message: '请输入字典标签', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_value: [
|
||||
{ required: true, message: '请输入字典值', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
parent_id: [
|
||||
{ type: 'number', min: 0, message: '父级ID必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
sort: [
|
||||
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
color: [
|
||||
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
icon: [
|
||||
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取字典类型列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 dictItem 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictItem,
|
||||
(newDictItem) => {
|
||||
if (newDictItem) {
|
||||
dictItemForm.id = newDictItem.id || null
|
||||
dictItemForm.dict_type_id = newDictItem.dict_type_id || null
|
||||
dictItemForm.dict_label = newDictItem.dict_label || ''
|
||||
dictItemForm.dict_value = newDictItem.dict_value || ''
|
||||
dictItemForm.parent_id = newDictItem.parent_id || 0
|
||||
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1
|
||||
dictItemForm.sort = newDictItem.sort || 0
|
||||
dictItemForm.color = newDictItem.color || ''
|
||||
dictItemForm.icon = newDictItem.icon || ''
|
||||
dictItemForm.remark = newDictItem.remark || ''
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听 dictTypeId 变化
|
||||
watch(
|
||||
() => props.dictTypeId,
|
||||
(newTypeId) => {
|
||||
if (newTypeId && !isEditing.value) {
|
||||
dictItemForm.dict_type_id = newTypeId
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictItemForm.id = null
|
||||
dictItemForm.dict_type_id = props.dictTypeId || null
|
||||
dictItemForm.dict_label = ''
|
||||
dictItemForm.dict_value = ''
|
||||
dictItemForm.parent_id = 0
|
||||
dictItemForm.status = 1
|
||||
dictItemForm.sort = 0
|
||||
dictItemForm.color = ''
|
||||
dictItemForm.icon = ''
|
||||
dictItemForm.remark = ''
|
||||
dictItemFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictItemFormRef.value) return
|
||||
|
||||
try {
|
||||
await dictItemFormRef.value.validate()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_type_id: dictItemForm.dict_type_id,
|
||||
dict_label: dictItemForm.dict_label,
|
||||
dict_value: dictItemForm.dict_value,
|
||||
parent_id: dictItemForm.parent_id,
|
||||
status: dictItemForm.status,
|
||||
sort: dictItemForm.sort,
|
||||
color: dictItemForm.color || '',
|
||||
icon: dictItemForm.icon || '',
|
||||
remark: dictItemForm.remark || '',
|
||||
}
|
||||
|
||||
if (isEditing.value) {
|
||||
const res = await updateDictItem(dictItemForm.id!, submitData)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('字典项更新成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.message || '更新失败,请重试')
|
||||
}
|
||||
} else {
|
||||
const res = await addDictItem(submitData)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('字典项添加成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.message || '添加失败,请重试')
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
<template>
|
||||
<div class="dict-item-list">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-select
|
||||
v-model="selectedDictTypeId"
|
||||
placeholder="选择字典类型"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 250px"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="`${type.dict_name} (${type.dict_code})`"
|
||||
:value="type.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索字典项标签或值"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!selectedDictTypeId"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<el-alert
|
||||
v-if="!selectedDictTypeId"
|
||||
title="请先选择字典类型"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchDictItems">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 字典项列表 -->
|
||||
<div v-else-if="selectedDictTypeId">
|
||||
<el-table
|
||||
:data="filteredDictItems"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_label" label="字典标签" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_value" label="字典值" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_value }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.parent_id || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="color" label="颜色" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.color" class="color-display">
|
||||
<span
|
||||
class="color-dot"
|
||||
:style="{ backgroundColor: row.color }"
|
||||
></span>
|
||||
<span>{{ row.color }}</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="icon" label="图标" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.icon">{{ row.icon }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<DictItemEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:dict-item="currentDictItem"
|
||||
:dict-type-id="selectedDictTypeId"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus'
|
||||
import { Plus, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
getDictItems,
|
||||
deleteDictItem,
|
||||
} from '@/api/dict'
|
||||
import DictItemEditDialog from './DictItemEditDialog.vue'
|
||||
|
||||
interface Props {
|
||||
selectedTypeId?: number | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
selectedTypeId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const dictTypes = ref<any[]>([])
|
||||
const dictItems = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref<number | ''>('')
|
||||
const selectedDictTypeId = ref<number | null>(null)
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const currentDictItem = ref<any>(null)
|
||||
|
||||
// 过滤后的字典项
|
||||
const filteredDictItems = computed(() => {
|
||||
let result = dictItems.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(item) =>
|
||||
(item.dict_label && item.dict_label.toLowerCase().includes(keyword)) ||
|
||||
(item.dict_value && item.dict_value.toLowerCase().includes(keyword)) ||
|
||||
(item.remark && item.remark.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value !== '') {
|
||||
result = result.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 监听外部传入的selectedTypeId
|
||||
watch(
|
||||
() => props.selectedTypeId,
|
||||
(newId) => {
|
||||
if (newId) {
|
||||
selectedDictTypeId.value = newId
|
||||
fetchDictItems()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 日期显示统一为:YYYY-MM-DD HH:mm:ss
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return String(dateStr)
|
||||
const p = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// 获取字典类型列表(用于下拉选择)
|
||||
async function fetchDictTypes() {
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取字典类型列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典项列表
|
||||
async function fetchDictItems() {
|
||||
if (!selectedDictTypeId.value) {
|
||||
dictItems.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: selectedDictTypeId.value })
|
||||
if (res.success === true && res.data) {
|
||||
dictItems.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictItems.value = res
|
||||
} else if (res && res.data) {
|
||||
dictItems.value = Array.isArray(res.data) ? res.data : []
|
||||
} else {
|
||||
error.value = res.message || '获取字典项列表失败'
|
||||
dictItems.value = []
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取字典项列表失败'
|
||||
dictItems.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 字典类型改变
|
||||
const handleTypeChange = () => {
|
||||
fetchDictItems()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const handleFilter = () => {
|
||||
// 筛选逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 设置字典类型ID(供外部调用)
|
||||
function setTypeId(typeId: number) {
|
||||
selectedDictTypeId.value = typeId
|
||||
}
|
||||
|
||||
// 添加字典项
|
||||
function handleAdd() {
|
||||
if (!selectedDictTypeId.value) {
|
||||
ElMessage.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
currentDictItem.value = null
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典项
|
||||
function handleEdit(row: any) {
|
||||
currentDictItem.value = { ...row }
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除字典项
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典项「${row.dict_label}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await deleteDictItem(row.id)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictItems()
|
||||
emit('refresh')
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchDictItems()
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
fetchDictTypes()
|
||||
if (selectedDictTypeId.value) {
|
||||
fetchDictItems()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
setTypeId,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
if (selectedDictTypeId.value) {
|
||||
fetchDictItems()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-item-list {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
|
||||
.color-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,241 +1,145 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典类型' : '添加字典类型'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictTypeForm"
|
||||
:rules="formRules"
|
||||
ref="dictTypeFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典编码" prop="dict_code">
|
||||
<el-input
|
||||
v-model="dictTypeForm.dict_code"
|
||||
placeholder="请输入字典编码(如:user_status)"
|
||||
:disabled="isEditing"
|
||||
/>
|
||||
<div class="form-tip">只能包含字母、数字或下划线,且必须唯一</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input
|
||||
v-model="dictTypeForm.dict_name"
|
||||
placeholder="请输入字典名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictTypeForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="shouldShowGlobalOption" label="全局展示" prop="is_global">
|
||||
<el-radio-group v-model="dictTypeForm.is_global">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">是:所有租户可见;否:仅租户内部可见</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序序号" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictTypeForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictTypeForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { addDictType, updateDictType, getDictTypes } from '@/api/dict';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
dictType?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictType: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const dictTypeFormRef = ref<FormInstance>();
|
||||
const authStore = useAuthStore();
|
||||
// 已移除父级选择
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictType && props.dictType.id);
|
||||
});
|
||||
|
||||
// 判断是否应该显示全局展示选项(只有 system_admin 和 admin 角色才能看到)
|
||||
const shouldShowGlobalOption = computed(() => {
|
||||
const roleCode = authStore.user.role_code;
|
||||
return roleCode === 'system_admin' || roleCode === 'admin';
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const dictTypeForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
status: 1,
|
||||
is_global: 0,
|
||||
sort: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '只能包含字母、数字或下划线',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_name: [
|
||||
{ required: true, message: '请输入字典名称', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序序号', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 已移除父级选择相关数据获取
|
||||
|
||||
// 监听 dictType 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictType,
|
||||
(newDictType) => {
|
||||
if (newDictType) {
|
||||
dictTypeForm.id = newDictType.id || null;
|
||||
dictTypeForm.dict_code = newDictType.dict_code || '';
|
||||
dictTypeForm.dict_name = newDictType.dict_name || '';
|
||||
dictTypeForm.status = newDictType.status !== undefined ? newDictType.status : 1;
|
||||
dictTypeForm.is_global = newDictType.is_global !== undefined ? newDictType.is_global : 0;
|
||||
dictTypeForm.sort = newDictType.sort || 0;
|
||||
dictTypeForm.remark = newDictType.remark || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="isEdit ? '编辑字典' : '新增字典'"
|
||||
width="520px"
|
||||
@update:model-value="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input v-model="form.dict_name" placeholder="如:供应商类型" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典编码" prop="dict_code">
|
||||
<el-input
|
||||
v-model="form.dict_code"
|
||||
:disabled="isEdit"
|
||||
placeholder="如:supplier_type(业务按编码取选项,保存后不可修改)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createDictType, updateDictType } from '@/api/dict'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
/** 传入表示编辑,null 表示新增 */
|
||||
dictType: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
dict_name: '',
|
||||
dict_code: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!form.id)
|
||||
|
||||
const rules = {
|
||||
dict_name: [
|
||||
{ required: true, message: '请输入字典名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
dict_code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
|
||||
message: '编码以字母开头,只能包含字母、数字与下划线',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 已移除父级选择监听
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictTypeForm.id = null;
|
||||
dictTypeForm.dict_code = '';
|
||||
dictTypeForm.dict_name = '';
|
||||
dictTypeForm.status = 1;
|
||||
dictTypeForm.is_global = 0;
|
||||
dictTypeForm.sort = 0;
|
||||
dictTypeForm.remark = '';
|
||||
dictTypeFormRef.value?.clearValidate();
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) initForm()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
)
|
||||
|
||||
function initForm() {
|
||||
const d = props.dictType
|
||||
form.id = d?.id || null
|
||||
form.dict_name = d?.dict_name || ''
|
||||
form.dict_code = d?.dict_code || ''
|
||||
form.sort = d?.sort ?? 0
|
||||
form.status = d?.status ?? 1
|
||||
form.remark = d?.remark || ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictTypeFormRef.value) return;
|
||||
|
||||
try {
|
||||
await dictTypeFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_code: dictTypeForm.dict_code,
|
||||
dict_name: dictTypeForm.dict_name,
|
||||
status: dictTypeForm.status,
|
||||
is_global: dictTypeForm.is_global,
|
||||
sort: dictTypeForm.sort,
|
||||
remark: dictTypeForm.remark || '',
|
||||
};
|
||||
|
||||
if (isEditing.value) {
|
||||
await updateDictType(dictTypeForm.id!, submitData);
|
||||
ElMessage.success('字典类型更新成功');
|
||||
} else {
|
||||
await addDictType(submitData);
|
||||
ElMessage.success('字典类型添加成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
|
||||
const payload = {
|
||||
dict_name: form.dict_name,
|
||||
sort: form.sort,
|
||||
status: form.status,
|
||||
remark: form.remark
|
||||
}
|
||||
// 新增时才提交编码;编辑时编码不可改,由后端忽略
|
||||
if (!isEdit.value) payload.dict_code = form.dict_code
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = isEdit.value
|
||||
? await updateDictType(form.id, payload)
|
||||
: await createDictType(payload)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '保存成功' : '新增成功')
|
||||
emit('update:visible', false)
|
||||
emit('success', isEdit.value ? { id: form.id } : res.data || null)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.response?.data?.msg || e?.message || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 无需加载父级数据
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
<template>
|
||||
<div class="dict-type-list">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索字典类型名称或编码"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典类型
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchDictTypes">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 字典类型列表 -->
|
||||
<div v-else>
|
||||
<el-table
|
||||
:data="filteredDictTypes"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tid" label="租户ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.tid || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.parent_id || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click.stop="handleViewItems(row)">
|
||||
<el-icon><Menu /></el-icon>
|
||||
查看字典项
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<DictTypeEdit
|
||||
v-model="editDialogVisible"
|
||||
:dict-type="currentDictType"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus'
|
||||
import { Plus, Search, Edit, Delete, Menu } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
deleteDictType,
|
||||
} from '@/api/dict'
|
||||
import DictTypeEdit from './DictTypeEdit.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-type': [typeId: number]
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const dictTypes = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref<number | ''>('')
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const currentDictType = ref<any>(null)
|
||||
|
||||
// 过滤后的字典类型
|
||||
const filteredDictTypes = computed(() => {
|
||||
let result = dictTypes.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(type) =>
|
||||
(type.dict_name && type.dict_name.toLowerCase().includes(keyword)) ||
|
||||
(type.dict_code && type.dict_code.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value !== '') {
|
||||
result = result.filter((type) => type.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 日期显示统一为:YYYY-MM-DD HH:mm:ss
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return String(dateStr)
|
||||
const p = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else {
|
||||
error.value = res.message || '获取字典类型列表失败'
|
||||
dictTypes.value = []
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取字典类型列表失败'
|
||||
dictTypes.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const handleFilter = () => {
|
||||
// 筛选逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
function handleAdd() {
|
||||
currentDictType.value = null
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典类型
|
||||
function handleEdit(row: any) {
|
||||
currentDictType.value = { ...row }
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 查看字典项
|
||||
function handleViewItems(row: any) {
|
||||
emit('select-type', row.id)
|
||||
}
|
||||
|
||||
// 行点击
|
||||
function handleRowClick(row: any) {
|
||||
// 可以在这里处理行点击事件
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictTypes()
|
||||
emit('refresh')
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchDictTypes()
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
fetchDictTypes()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-type-list {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,357 +1,517 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>字典管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 内容区域:根据当前视图切换 -->
|
||||
<div v-if="currentView === 'types'">
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="handleAddType">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典类型
|
||||
</el-button>
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="typeSearchKeyword"
|
||||
placeholder="搜索字典类型名称或编码"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleTypeSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="dictTypes"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="typeLoading"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.parent_id > 0">{{ row.parent_id }}</span>
|
||||
<span v-else style="color: #999">-</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_global" label="全局展示" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_global === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ row.is_global === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="sort" label="排序" width="80" align="center" /> -->
|
||||
<!-- <el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip /> -->
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEditType(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleViewItems(row)">
|
||||
<el-icon><List /></el-icon>
|
||||
查看字典项
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDeleteType(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="totalCount"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentView === 'items'">
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<el-button @click="goBackToTypes">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<span v-if="selectedDictType">
|
||||
当前字典类型:{{ selectedDictType.dict_name }}({{ selectedDictType.dict_code }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="dict-page">
|
||||
<!-- ================= 一级:字典列表 ================= -->
|
||||
<template v-if="view === 'list'">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>数据字典</h2>
|
||||
<p>维护系统下拉选项:先建字典,再进「字典内容」维护字典项。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="openTypeCreate">新增字典</el-button>
|
||||
<el-button :icon="Refresh" @click="loadTypes">刷新</el-button>
|
||||
</div>
|
||||
<DictItemList :selected-type-id="selectedDictTypeId || undefined" @refresh="refreshTypesSilently" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字典类型编辑对话框 -->
|
||||
<DictTypeEdit
|
||||
v-model="typeDialogVisible"
|
||||
:dict-type="currentDictType"
|
||||
@success="handleTypeSuccess"
|
||||
<div class="dict-panel">
|
||||
<div class="panel-header">
|
||||
<el-input
|
||||
v-model="typeKeyword"
|
||||
class="panel-search"
|
||||
placeholder="搜索字典名称 / 编码"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
@keyup.enter="handleTypeSearch"
|
||||
@clear="handleTypeSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="types"
|
||||
class="dict-table"
|
||||
stripe
|
||||
v-loading="typeLoading"
|
||||
>
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.dict_name }}</span>
|
||||
<!-- 全局内置字典:所有租户共享、只读(平台端无此字段,自动不显示) -->
|
||||
<el-tag v-if="row.is_global" class="inline-tag" size="small" type="warning" effect="plain">
|
||||
内置
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="说明" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="175" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openItems(row)">字典内容</el-button>
|
||||
<el-button link type="primary" size="small" :disabled="row.is_global" @click="openTypeEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" :disabled="row.is_global" @click="handleTypeDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="panel-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="typePage.page"
|
||||
v-model:page-size="typePage.pageSize"
|
||||
:total="typePage.total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadTypes"
|
||||
@current-change="loadTypes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================= 二级:字典项列表 ================= -->
|
||||
<template v-else>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item>
|
||||
<a class="crumb-link" @click="goBackToList">数据字典</a>
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>字典项</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<h2 class="sub-title">
|
||||
{{ currentType?.dict_name }}
|
||||
<el-tag size="small" effect="plain">{{ currentType?.dict_code }}</el-tag>
|
||||
<el-tag v-if="isGlobalDict" size="small" type="warning" effect="plain">内置只读</el-tag>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="ArrowLeft" @click="goBackToList">返回</el-button>
|
||||
<el-button type="primary" :icon="Plus" :disabled="isGlobalDict" @click="openItemCreate">
|
||||
新增字典项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dict-panel">
|
||||
<div class="panel-header">
|
||||
<el-input
|
||||
v-model="itemKeyword"
|
||||
class="panel-search"
|
||||
placeholder="搜索字典项名称 / 值"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
@keyup.enter="handleItemSearch"
|
||||
@clear="handleItemSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="items"
|
||||
class="dict-table"
|
||||
stripe
|
||||
border
|
||||
v-loading="itemLoading"
|
||||
>
|
||||
<el-table-column prop="item_label" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="item_value" label="值" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ row.item_value }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tag_type" label="标签色" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.tag_type" :type="row.tag_type" size="small">{{ row.tag_type }}</el-tag>
|
||||
<span v-else class="muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="is_default" label="默认" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_default === 1" type="success" size="small">默认</el-tag>
|
||||
<span v-else class="muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" :disabled="isGlobalDict" @click="openItemEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" :disabled="isGlobalDict" @click="handleItemToggle(row)">
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" :disabled="isGlobalDict" @click="handleItemDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="panel-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="itemPage.page"
|
||||
v-model:page-size="itemPage.pageSize"
|
||||
:total="itemPage.total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadItems"
|
||||
@current-change="loadItems"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<DictTypeEdit v-model="typeDialogVisible" :dict-type="currentDictType" @success="handleTypeSaved" />
|
||||
|
||||
<DictItemEdit
|
||||
v-model="itemDialogVisible"
|
||||
:dict-id="currentType?.id"
|
||||
:dict-item="currentDictItem"
|
||||
@success="loadItems"
|
||||
/>
|
||||
|
||||
<!-- 字典项独立页面内操作通过 DictItemList 完成 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
<script setup>
|
||||
import { computed, ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Refresh,
|
||||
Search,
|
||||
List,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { Plus, Search, Refresh, ArrowLeft } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
deleteDictType,
|
||||
getDictItems,
|
||||
deleteDictItem,
|
||||
toggleDictItemStatus
|
||||
} from '@/api/dict'
|
||||
import DictTypeEdit from './components/DictTypeEdit.vue'
|
||||
import DictItemList from './components/DictItemList.vue'
|
||||
import DictItemEdit from './components/DictItemEdit.vue'
|
||||
import { formatDateTime } from '@/utils/datetime'
|
||||
|
||||
const currentView = ref<'types' | 'items'>('types')
|
||||
/** 当前视图:list 字典列表 / items 字典项列表 */
|
||||
const view = ref('list')
|
||||
|
||||
/* ---------------- 字典列表 ---------------- */
|
||||
|
||||
const typeLoading = ref(false)
|
||||
|
||||
// 字典类型相关
|
||||
const dictTypes = ref<any[]>([])
|
||||
const typeSearchKeyword = ref('')
|
||||
const types = ref([])
|
||||
const typeKeyword = ref('')
|
||||
const typeDialogVisible = ref(false)
|
||||
const currentDictType = ref<any>(null)
|
||||
const currentDictType = ref(null)
|
||||
const typePage = reactive({ page: 1, pageSize: 15, total: 0 })
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalCount = ref(0)
|
||||
/* ---------------- 字典项 ---------------- */
|
||||
|
||||
const selectedDictTypeId = ref<number | null>(null)
|
||||
const selectedDictType = computed(() => {
|
||||
if (!selectedDictTypeId.value) {
|
||||
return null
|
||||
}
|
||||
return dictTypes.value.find((type) => type.id === selectedDictTypeId.value) || null
|
||||
})
|
||||
const itemLoading = ref(false)
|
||||
const items = ref([])
|
||||
const itemKeyword = ref('')
|
||||
const itemDialogVisible = ref(false)
|
||||
const currentDictItem = ref(null)
|
||||
const itemPage = reactive({ page: 1, pageSize: 15, total: 0 })
|
||||
|
||||
// 日期显示统一为:YYYY-MM-DD HH:mm:ss(utils/datetime)
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
return dateStr ? formatDateTime(dateStr) : ''
|
||||
/** 进入字典项页时选中的字典 */
|
||||
const currentType = ref(null)
|
||||
|
||||
/** 后端失败时优先透出业务提示(兼容 {code,msg} 与 HTTP 错误两种形态) */
|
||||
function errMsg(e, fallback) {
|
||||
return e?.response?.data?.msg || e?.message || fallback
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
/** 全局内置字典:只读(平台端字段为 undefined,自然放开) */
|
||||
const isGlobalDict = computed(() => !!currentType.value?.is_global)
|
||||
|
||||
onMounted(loadTypes)
|
||||
|
||||
async function loadTypes() {
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await getDictTypes({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: typeSearchKeyword.value
|
||||
page: typePage.page,
|
||||
pageSize: typePage.pageSize,
|
||||
keyword: typeKeyword.value
|
||||
})
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data.list) ? res.data.list : (Array.isArray(res.data) ? res.data : [])
|
||||
totalCount.value = res.data.total || res.data.length || 0
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
totalCount.value = res.length
|
||||
if (res.code === 200) {
|
||||
types.value = res.data?.list || []
|
||||
typePage.total = res.data?.total || 0
|
||||
} else {
|
||||
dictTypes.value = []
|
||||
totalCount.value = 0
|
||||
ElMessage.error(res.message || '获取字典类型失败')
|
||||
ElMessage.error(res.msg || '获取字典列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取字典类型失败')
|
||||
dictTypes.value = []
|
||||
totalCount.value = 0
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '获取字典列表失败'))
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 字典类型搜索
|
||||
function handleTypeSearch() {
|
||||
currentPage.value = 1
|
||||
fetchDictTypes()
|
||||
typePage.page = 1
|
||||
loadTypes()
|
||||
}
|
||||
|
||||
// 分页切换
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
fetchDictTypes()
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
function handleAddType() {
|
||||
function openTypeCreate() {
|
||||
currentDictType.value = null
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典类型
|
||||
function handleEditType(row: any) {
|
||||
function openTypeEdit(row) {
|
||||
currentDictType.value = { ...row }
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 查看字典项
|
||||
function handleViewItems(row: any) {
|
||||
selectedDictTypeId.value = row.id
|
||||
currentView.value = 'items'
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
async function handleDeleteType(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success === true) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictTypes()
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
/** 字典保存后刷新列表;新增的字典直接进入它的字典项 */
|
||||
async function handleTypeSaved(created) {
|
||||
await loadTypes()
|
||||
if (created?.id) {
|
||||
const hit = types.value.find((t) => t.id === created.id) || created
|
||||
openItems(hit)
|
||||
}
|
||||
}
|
||||
|
||||
// 字典类型操作成功回调
|
||||
function handleTypeSuccess() {
|
||||
fetchDictTypes()
|
||||
async function handleTypeDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典「${row.dict_name}」吗?删除后不可恢复,其下的字典项也会一并失效。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
loadTypes()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '删除失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 从字典项页返回
|
||||
function goBackToTypes() {
|
||||
currentView.value = 'types'
|
||||
/* ---------------- 字典项 ---------------- */
|
||||
|
||||
/** 进入某个字典的字典项列表 */
|
||||
function openItems(row) {
|
||||
currentType.value = { ...row }
|
||||
itemKeyword.value = ''
|
||||
itemPage.page = 1
|
||||
view.value = 'items'
|
||||
loadItems()
|
||||
}
|
||||
|
||||
// 子页面刷新后,静默刷新类型列表
|
||||
function refreshTypesSilently() {
|
||||
fetchDictTypes()
|
||||
function goBackToList() {
|
||||
view.value = 'list'
|
||||
if (currentType.value) {
|
||||
loadTypes() // 回列表时同步一下(字典项数量可能变了)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
async function refresh() {
|
||||
await fetchDictTypes()
|
||||
ElMessage.success('刷新成功')
|
||||
async function loadItems() {
|
||||
if (!currentType.value?.id) return
|
||||
itemLoading.value = true
|
||||
try {
|
||||
const res = await getDictItems({
|
||||
dict_id: currentType.value.id,
|
||||
page: itemPage.page,
|
||||
pageSize: itemPage.pageSize,
|
||||
keyword: itemKeyword.value
|
||||
})
|
||||
if (res.code === 200) {
|
||||
items.value = res.data?.list || []
|
||||
itemPage.total = res.data?.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取字典项失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '获取字典项失败'))
|
||||
} finally {
|
||||
itemLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
function handleItemSearch() {
|
||||
itemPage.page = 1
|
||||
loadItems()
|
||||
}
|
||||
|
||||
function openItemCreate() {
|
||||
if (!currentType.value) return
|
||||
currentDictItem.value = null
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openItemEdit(row) {
|
||||
currentDictItem.value = { ...row }
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleItemToggle(row) {
|
||||
const next = row.status === 1 ? 0 : 1
|
||||
try {
|
||||
const res = await toggleDictItemStatus(row.id, next)
|
||||
if (res.code === 200) {
|
||||
row.status = next
|
||||
ElMessage.success(next === 1 ? '已启用' : '已停用')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '操作失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleItemDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典项「${row.item_label}」吗?删除后不可恢复。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await deleteDictItem(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
loadItems()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '删除失败'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container-box {
|
||||
<style lang="less" scoped>
|
||||
.dict-page {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px 0;
|
||||
.crumb-link {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 400;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.dict-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.panel-search {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.filter-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 表格高度自适应:行数少时按内容收缩,行数多时封顶并在表格内部滚动(表头常驻) */
|
||||
.dict-table {
|
||||
:deep(.el-table__body-wrapper) {
|
||||
max-height: calc(100vh - 440px);
|
||||
min-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
.panel-pagination {
|
||||
flex-shrink: 0;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.muted {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e5e5e5;
|
||||
display: inline-block;
|
||||
.inline-tag {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* 屏幕很矮 / 窄屏时放宽封顶高度,交给页面滚动 */
|
||||
@media (max-height: 620px), (max-width: 900px) {
|
||||
.dict-table {
|
||||
:deep(.el-table__body-wrapper) {
|
||||
max-height: 360px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user