提交
This commit is contained in:
1 parent
b45ce2c32c
commit
a5210a6a9e
30 files changed
+11989
-5917
No files matched your search
@@ -129,11 +129,11 @@
|
||||
"127": "selectContractRow",
|
||||
"128": "Community 128",
|
||||
"129": "Community 129",
|
||||
"130": "emit",
|
||||
"130": "prepareLineAliasAndBrandForSubmit",
|
||||
"131": "prepareLineAliasAndBrandForSubmit",
|
||||
"132": "Community 132",
|
||||
"133": "Community 133",
|
||||
"134": "index.vue",
|
||||
"134": "VersionManager",
|
||||
"135": "detail.vue",
|
||||
"136": "Community 136",
|
||||
"137": "Community 137",
|
||||
@@ -146,28 +146,28 @@
|
||||
"144": "Community 144",
|
||||
"145": "api",
|
||||
"146": "price-letter-template.ts",
|
||||
"147": "loading.ts",
|
||||
"147": "contractType.ts",
|
||||
"148": "Community 148",
|
||||
"149": "fetchCustomerDetail",
|
||||
"150": "Community 150",
|
||||
"151": "Community 151",
|
||||
"152": "SettingDrawer.vue",
|
||||
"153": "index.vue",
|
||||
"154": "detail.vue",
|
||||
"154": "index.vue",
|
||||
"155": "contractType.ts",
|
||||
"156": "v2.5.7 - 2025-09-14",
|
||||
"157": "form.vue",
|
||||
"158": "removeProductsNotAllowedByHazard",
|
||||
"159": "detail.vue",
|
||||
"160": "product.ts",
|
||||
"160": "crm-owner-candidates.ts",
|
||||
"161": "Community 161",
|
||||
"162": "Community 162",
|
||||
"163": "pool.ts",
|
||||
"163": "index.vue",
|
||||
"164": "Community 164",
|
||||
"165": "handleDisburse",
|
||||
"166": "Community 166",
|
||||
"167": "Community 167",
|
||||
"168": "AssignDialog.vue",
|
||||
"168": "index.vue",
|
||||
"169": "Community 169",
|
||||
"170": "Community 170",
|
||||
"171": "index.vue",
|
||||
@@ -182,28 +182,19 @@
|
||||
"180": "index.vue",
|
||||
"181": "Community 181",
|
||||
"182": "Community 182",
|
||||
"183": "StorageConfig",
|
||||
"184": "system.ts",
|
||||
"185": "SettingActions.vue",
|
||||
"186": "StorageKeyManager",
|
||||
"187": "Community 187",
|
||||
"188": "Community 188",
|
||||
"189": "Community 189",
|
||||
"190": "ViewKvTableCell.vue",
|
||||
"191": "handleConfirmApproval",
|
||||
"192": "index.vue",
|
||||
"193": "navigation.ts",
|
||||
"194": "canDeleteByCreator",
|
||||
"195": "Community 195",
|
||||
"196": "Community 196",
|
||||
"197": "v2.4.2.1 - 2025-06-16",
|
||||
"198": "index.vue",
|
||||
"200": "index.js",
|
||||
"201": "index.vue",
|
||||
"201": "menuFavorite.ts",
|
||||
"202": "Community 202",
|
||||
"206": "index.vue",
|
||||
"206": "form.vue",
|
||||
"207": "Community 207",
|
||||
"208": "Community 208",
|
||||
"209": "Community 209",
|
||||
"210": "Community 210",
|
||||
"212": "index.js",
|
||||
@@ -553,13 +544,11 @@
|
||||
"1275": "index.js",
|
||||
"1277": "plugin.js",
|
||||
"1278": "plugin.min.js",
|
||||
"1279": "index.js",
|
||||
"1280": "plugin.js",
|
||||
"1281": "plugin.min.js",
|
||||
"1282": "index.js",
|
||||
"1283": "plugin.js",
|
||||
"1284": "plugin.min.js",
|
||||
"1286": "index.js",
|
||||
"1287": "plugin.js",
|
||||
"1288": "plugin.min.js",
|
||||
"1289": "index.js",
|
||||
|
||||
@@ -129,11 +129,10 @@
|
||||
"127": "selectContractRow",
|
||||
"128": "Community 128",
|
||||
"129": "Community 129",
|
||||
"130": "emit",
|
||||
"131": "prepareLineAliasAndBrandForSubmit",
|
||||
"132": "Community 132",
|
||||
"133": "Community 133",
|
||||
"134": "index.vue",
|
||||
"134": "VersionManager",
|
||||
"135": "detail.vue",
|
||||
"136": "Community 136",
|
||||
"137": "Community 137",
|
||||
@@ -146,28 +145,24 @@
|
||||
"144": "Community 144",
|
||||
"145": "api",
|
||||
"146": "price-letter-template.ts",
|
||||
"147": "loading.ts",
|
||||
"148": "Community 148",
|
||||
"149": "fetchCustomerDetail",
|
||||
"150": "Community 150",
|
||||
"151": "Community 151",
|
||||
"152": "SettingDrawer.vue",
|
||||
"153": "index.vue",
|
||||
"154": "detail.vue",
|
||||
"154": "index.vue",
|
||||
"155": "contractType.ts",
|
||||
"156": "v2.5.7 - 2025-09-14",
|
||||
"157": "form.vue",
|
||||
"158": "removeProductsNotAllowedByHazard",
|
||||
"159": "detail.vue",
|
||||
"160": "product.ts",
|
||||
"161": "Community 161",
|
||||
"162": "Community 162",
|
||||
"163": "pool.ts",
|
||||
"164": "Community 164",
|
||||
"165": "handleDisburse",
|
||||
"166": "Community 166",
|
||||
"167": "Community 167",
|
||||
"168": "AssignDialog.vue",
|
||||
"169": "Community 169",
|
||||
"170": "Community 170",
|
||||
"171": "index.vue",
|
||||
@@ -184,21 +179,20 @@
|
||||
"182": "Community 182",
|
||||
"183": "StorageConfig",
|
||||
"184": "system.ts",
|
||||
"185": "SettingActions.vue",
|
||||
"186": "StorageKeyManager",
|
||||
"187": "Community 187",
|
||||
"188": "Community 188",
|
||||
"189": "Community 189",
|
||||
"190": "ViewKvTableCell.vue",
|
||||
"191": "handleConfirmApproval",
|
||||
"194": "highlight.ts",
|
||||
"195": "Community 195",
|
||||
"196": "Community 196",
|
||||
"200": "index.js",
|
||||
"201": "index.vue",
|
||||
"201": "menuFavorite.ts",
|
||||
"202": "Community 202",
|
||||
"206": "index.vue",
|
||||
"206": "form.vue",
|
||||
"207": "Community 207",
|
||||
"208": "Community 208",
|
||||
"209": "Community 209",
|
||||
"210": "Community 210",
|
||||
"212": "index.js",
|
||||
@@ -554,7 +548,6 @@
|
||||
"1282": "index.js",
|
||||
"1283": "plugin.js",
|
||||
"1284": "plugin.min.js",
|
||||
"1286": "index.js",
|
||||
"1287": "plugin.js",
|
||||
"1288": "plugin.min.js",
|
||||
"1289": "index.js",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+2604
-2329
File diff suppressed because it is too large.
Load diff
@@ -60,10 +60,12 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const fileList = ref<UploadFile[]>([])
|
||||
/** 本地 URL 列表,避免多文件并行上传完成时基于 stale props 互相覆盖 */
|
||||
const localUrls = ref<string[]>([])
|
||||
|
||||
const isDirectUrl = (value: string) => /^https?:\/\//i.test(value)
|
||||
|
||||
const displayItems = computed(() => parseValue(props.modelValue))
|
||||
const displayItems = computed(() => localUrls.value)
|
||||
|
||||
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','))
|
||||
|
||||
@@ -83,7 +85,7 @@ function listToString(items: string[]) {
|
||||
}
|
||||
|
||||
function syncFileListFromModel() {
|
||||
fileList.value = displayItems.value.map((url, index) => ({
|
||||
fileList.value = localUrls.value.map((url, index) => ({
|
||||
name: url.split('/').pop() || `file-${index + 1}`,
|
||||
url,
|
||||
status: 'success'
|
||||
@@ -92,7 +94,8 @@ function syncFileListFromModel() {
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
(value) => {
|
||||
localUrls.value = parseValue(value)
|
||||
if (props.disabled) return
|
||||
syncFileListFromModel()
|
||||
},
|
||||
@@ -124,7 +127,8 @@ async function handleUploadRequest(options: UploadRequestOptions) {
|
||||
if (!fileUrl) {
|
||||
throw new Error('上传失败')
|
||||
}
|
||||
const next = [...displayItems.value, fileUrl]
|
||||
const next = [...localUrls.value, fileUrl]
|
||||
localUrls.value = next
|
||||
emit('update:modelValue', listToString(next))
|
||||
options.onSuccess?.(res)
|
||||
} catch (error: any) {
|
||||
@@ -136,7 +140,8 @@ async function handleUploadRequest(options: UploadRequestOptions) {
|
||||
function handleRemove(uploadFile: UploadFile) {
|
||||
const removedUrl = uploadFile.url || (uploadFile.response as { fileUrl?: string })?.fileUrl
|
||||
if (!removedUrl) return
|
||||
const next = displayItems.value.filter((url) => url !== removedUrl)
|
||||
const next = localUrls.value.filter((url) => url !== removedUrl)
|
||||
localUrls.value = next
|
||||
emit('update:modelValue', listToString(next))
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
Plus,
|
||||
Loading,
|
||||
@@ -177,13 +177,23 @@
|
||||
const isPointerInside = ref(false)
|
||||
const isDragOver = ref(false)
|
||||
let dragEnterDepth = 0
|
||||
let clearUploadScheduled = false
|
||||
/**
|
||||
* 本地缓冲:el-upload 多选会同步连续触发 @change;
|
||||
* 若基于尚未回写的 props.modelValue 展开累加,会只留下最后一个文件。
|
||||
*/
|
||||
const files = ref<BxAttachmentItem[]>([])
|
||||
|
||||
const files = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
files.value = Array.isArray(val) ? val.slice() : []
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function syncFiles(next: BxAttachmentItem[]) {
|
||||
files.value = next
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
|
||||
@@ -256,12 +266,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: UploadFile) {
|
||||
const rawFile = uploadFile.raw
|
||||
if (!rawFile) return
|
||||
await uploadRawFile(rawFile)
|
||||
async function scheduleClearUploadFiles() {
|
||||
if (clearUploadScheduled) return
|
||||
clearUploadScheduled = true
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
clearUploadScheduled = false
|
||||
}
|
||||
|
||||
function handleAttachmentChange(uploadFile: UploadFile) {
|
||||
const rawFile = uploadFile.raw
|
||||
if (!rawFile) return
|
||||
void uploadRawFile(rawFile)
|
||||
void scheduleClearUploadFiles()
|
||||
}
|
||||
|
||||
function handlePointerEnter() {
|
||||
|
||||
@@ -95,12 +95,16 @@
|
||||
const isPointerInside = ref(false)
|
||||
const isDragOver = ref(false)
|
||||
let dragEnterDepth = 0
|
||||
let clearUploadScheduled = false
|
||||
const previewUrlMap = ref<Record<string, string>>({})
|
||||
|
||||
const files = computed(() => props.modelValue || [])
|
||||
/**
|
||||
* 本地缓冲:el-upload 多选时会同步连续触发多次 @change,
|
||||
* 若每次都基于尚未回写的 props.modelValue 累加,最终只会留下最后一个文件。
|
||||
*/
|
||||
const localFiles = ref<File[]>([])
|
||||
|
||||
const displayItems = computed<DisplayItem[]>(() =>
|
||||
files.value.map((file, index) => {
|
||||
localFiles.value.map((file, index) => {
|
||||
const key = `${file.name}-${file.size}-${file.lastModified}-${index}`
|
||||
return {
|
||||
key,
|
||||
@@ -119,7 +123,7 @@
|
||||
function rebuildPreviews() {
|
||||
revokeAllPreviews()
|
||||
const next: Record<string, string> = {}
|
||||
files.value.forEach((file, index) => {
|
||||
localFiles.value.forEach((file, index) => {
|
||||
if (!file.type.startsWith('image/')) return
|
||||
const key = `${file.name}-${file.size}-${file.lastModified}-${index}`
|
||||
next[key] = URL.createObjectURL(file)
|
||||
@@ -129,12 +133,17 @@
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => rebuildPreviews(),
|
||||
{ deep: true, immediate: true }
|
||||
(val) => {
|
||||
localFiles.value = Array.isArray(val) ? val.slice() : []
|
||||
rebuildPreviews()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function syncFiles(next: File[]) {
|
||||
localFiles.value = next
|
||||
emit('update:modelValue', next)
|
||||
rebuildPreviews()
|
||||
}
|
||||
|
||||
function isFileAccepted(file: File) {
|
||||
@@ -181,7 +190,7 @@
|
||||
|
||||
function appendFiles(incoming: File[]) {
|
||||
if (!incoming.length || props.disabled) return
|
||||
const next = [...files.value]
|
||||
const next = [...localFiles.value]
|
||||
for (const raw of incoming) {
|
||||
if (next.length >= props.limit) {
|
||||
ElMessage.warning(`最多上传 ${props.limit} 个文件`)
|
||||
@@ -196,16 +205,23 @@
|
||||
syncFiles(next)
|
||||
}
|
||||
|
||||
async function onChange(uploadFile: UploadFile) {
|
||||
async function scheduleClearUploadFiles() {
|
||||
if (clearUploadScheduled) return
|
||||
clearUploadScheduled = true
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
clearUploadScheduled = false
|
||||
}
|
||||
|
||||
function onChange(uploadFile: UploadFile) {
|
||||
const raw = uploadFile.raw
|
||||
if (!raw) return
|
||||
appendFiles([raw])
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
void scheduleClearUploadFiles()
|
||||
}
|
||||
|
||||
function removeAt(index: number) {
|
||||
const next = [...files.value]
|
||||
const next = [...localFiles.value]
|
||||
next.splice(index, 1)
|
||||
syncFiles(next)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
type="button"
|
||||
class="invoice-attach-card"
|
||||
:class="{ 'is-selected': selectedKeys.has(item.key) }"
|
||||
:style="{ '--invoice-group-color': invoiceGroupColor(item.invoiceId) }"
|
||||
@click="toggleOne(item.key)"
|
||||
>
|
||||
<span class="invoice-attach-card__index">{{ item.serial }}</span>
|
||||
@@ -48,7 +49,7 @@
|
||||
</span>
|
||||
<div class="invoice-attach-card__footer">
|
||||
<span class="invoice-attach-card__amount">
|
||||
{{ formatMoneyYuan(item.amount) }} 元
|
||||
整票 {{ formatMoneyYuan(item.amount) }} 元
|
||||
</span>
|
||||
<el-icon
|
||||
class="invoice-attach-card__download"
|
||||
@@ -179,6 +180,25 @@
|
||||
return `${y}年${Number(m)}月`
|
||||
}
|
||||
|
||||
/** 同一发票(同一次上传)共用边框色,不同发票互相区分 */
|
||||
const INVOICE_GROUP_COLORS = [
|
||||
'#0891b2',
|
||||
'#7c3aed',
|
||||
'#d97706',
|
||||
'#db2777',
|
||||
'#2563eb',
|
||||
'#059669',
|
||||
'#ea580c',
|
||||
'#4f46e5'
|
||||
]
|
||||
|
||||
function invoiceGroupColor(invoiceId: string | number) {
|
||||
const s = String(invoiceId || '')
|
||||
let hash = 0
|
||||
for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) >>> 0
|
||||
return INVOICE_GROUP_COLORS[hash % INVOICE_GROUP_COLORS.length]
|
||||
}
|
||||
|
||||
function invoiceAttachments(row: any): BxAttachmentItem[] {
|
||||
const list = Array.isArray(row?.attachments) ? row.attachments : []
|
||||
if (list.length) {
|
||||
@@ -314,37 +334,34 @@
|
||||
position: relative;
|
||||
width: 140px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border, #d9e8e2);
|
||||
border: 2px solid var(--invoice-group-color, var(--border, #d9e8e2));
|
||||
border-radius: 8px;
|
||||
background: var(--surface, #fff);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.12);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-attach-card__index {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 6px;
|
||||
left: 8px;
|
||||
font-size: 12px;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 8px 0 8px 0;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--primary, #10b981);
|
||||
background: none;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invoice-attach-card__preview {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
/**
|
||||
* 前端路由(报销管理、CRM 管理、地磅管理、工作流管理、系统管理;运输见 pl.ts)
|
||||
* 与后端菜单对齐时请同步 `sys_menu`(bx 见 docs/sql/20260606_bx_menu.sql;db 见 docs/sql/20260607_db_menu.sql)。
|
||||
* 与后端菜单对齐时请同步 `sys_menu`(bx / db 见历史迁移;系统管理二级分组见 docs/sql/migrations/2026-08-09-system-menu-groups.sql)。
|
||||
*/
|
||||
export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
// ──────────────────────────────────────────────
|
||||
@@ -608,222 +608,248 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: '101',
|
||||
name: 'Tenant',
|
||||
path: 'tenant',
|
||||
component: '/system/tenant/index',
|
||||
id: 'SYSM000000000000000000000000000010',
|
||||
name: 'SystemInfo',
|
||||
path: 'info',
|
||||
meta: {
|
||||
title: '租户管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:tenant:list'
|
||||
}
|
||||
title: '系统信息',
|
||||
keepAlive: false
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: '101',
|
||||
name: 'Tenant',
|
||||
path: 'tenant',
|
||||
component: '/system/tenant/index',
|
||||
meta: {
|
||||
title: '租户管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:tenant:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '102',
|
||||
name: 'Dict',
|
||||
path: 'dict',
|
||||
component: '/system/dict/index',
|
||||
meta: {
|
||||
title: '字典管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:dict:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '106',
|
||||
name: 'Menu',
|
||||
path: 'menu',
|
||||
component: '/system/menu/index',
|
||||
meta: {
|
||||
title: '菜单管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:menu:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '103',
|
||||
name: 'Dept',
|
||||
path: 'dept',
|
||||
component: '/system/dept/index',
|
||||
meta: {
|
||||
title: '组织管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:dept:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '105',
|
||||
name: 'Role',
|
||||
path: 'role',
|
||||
component: '/system/role/index',
|
||||
meta: {
|
||||
title: '角色管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:role:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '104',
|
||||
name: 'User',
|
||||
path: 'user',
|
||||
component: '/system/user/index',
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:user:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '108',
|
||||
name: 'Post',
|
||||
path: 'post',
|
||||
component: '/system/post/index',
|
||||
meta: {
|
||||
title: '职位管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:post:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'SYS_OPERLOG',
|
||||
name: 'SystemOperlog',
|
||||
path: 'operlog',
|
||||
component: '/system/operlog/index',
|
||||
meta: {
|
||||
title: '操作日志',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:operlog:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '10000000000000000000000000000008',
|
||||
name: 'UserDetail',
|
||||
path: 'user/detail',
|
||||
component: '/system/user/detail',
|
||||
meta: { title: '用户详情', isHide: true, keepAlive: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '102',
|
||||
name: 'Dict',
|
||||
path: 'dict',
|
||||
component: '/system/dict/index',
|
||||
id: 'SYSM000000000000000000000000000011',
|
||||
name: 'SystemTemp',
|
||||
path: 'temp',
|
||||
meta: {
|
||||
title: '字典管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:dict:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '106',
|
||||
name: 'Menu',
|
||||
path: 'menu',
|
||||
component: '/system/menu/index',
|
||||
meta: {
|
||||
title: '菜单管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:menu:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '103',
|
||||
name: 'Dept',
|
||||
path: 'dept',
|
||||
component: '/system/dept/index',
|
||||
meta: {
|
||||
title: '组织管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:dept:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '105',
|
||||
name: 'Role',
|
||||
path: 'role',
|
||||
component: '/system/role/index',
|
||||
meta: {
|
||||
title: '角色管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:role:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '104',
|
||||
name: 'User',
|
||||
path: 'user',
|
||||
component: '/system/user/index',
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:user:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '108',
|
||||
name: 'Post',
|
||||
path: 'post',
|
||||
component: '/system/post/index',
|
||||
meta: {
|
||||
title: '职位管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:post:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'SYS_OPERLOG',
|
||||
name: 'SystemOperlog',
|
||||
path: 'operlog',
|
||||
component: '/system/operlog/index',
|
||||
meta: {
|
||||
title: '操作日志',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:operlog:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'SYS_APPROVER_SETTING',
|
||||
name: 'SystemApproverSetting',
|
||||
path: 'approver-setting',
|
||||
component: '/system/approver-setting/index',
|
||||
meta: {
|
||||
title: '报价、合同审批人设置',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:approverSetting'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000010',
|
||||
name: 'CrmClue',
|
||||
path: 'clue',
|
||||
component: '/crm/clue/index',
|
||||
meta: { title: '线索管理', keepAlive: true, menuCode: 'crm:clue:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000011',
|
||||
name: 'CrmClueDetail',
|
||||
path: 'clue/detail/:id',
|
||||
component: '/crm/clue/detail',
|
||||
meta: {
|
||||
title: '线索详情',
|
||||
keepAlive: false,
|
||||
isHide: true,
|
||||
menuCode: 'crm:clue:query'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000012',
|
||||
name: 'CrmClueStatistics',
|
||||
path: 'clue/statistics',
|
||||
component: '/crm/clue/statistics/index',
|
||||
meta: {
|
||||
title: '线索统计',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:clue:statistics'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000010',
|
||||
name: 'CrmOpportunity',
|
||||
path: 'opportunity',
|
||||
component: '/crm/opportunity/index',
|
||||
meta: { title: '商机管理', keepAlive: true, menuCode: 'crm:opportunity:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000011',
|
||||
name: 'CrmOpportunityDetail',
|
||||
path: 'opportunity/detail/:id',
|
||||
component: '/crm/opportunity/detail',
|
||||
meta: {
|
||||
title: '商机详情',
|
||||
keepAlive: false,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:query'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000012',
|
||||
name: 'CrmOpportunityKanban',
|
||||
path: 'opportunity/kanban',
|
||||
component: '/crm/opportunity/kanban',
|
||||
meta: {
|
||||
title: '商机看板',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000013',
|
||||
name: 'CrmOpportunityFunnel',
|
||||
path: 'opportunity/funnel',
|
||||
component: '/crm/opportunity/funnel',
|
||||
meta: {
|
||||
title: '销售漏斗',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000014',
|
||||
name: 'CrmOpportunityStatistics',
|
||||
path: 'opportunity/statistics',
|
||||
component: '/crm/opportunity/statistics',
|
||||
meta: {
|
||||
title: '商机统计',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000003',
|
||||
name: 'CrmCustomerPool',
|
||||
path: 'customer-pool',
|
||||
component: '/crm/customer/pool/index',
|
||||
meta: { title: '公海池', keepAlive: true, menuCode: 'crm:customerPool:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000004',
|
||||
name: 'CrmPoolRule',
|
||||
path: 'pool-rule',
|
||||
component: '/crm/customer/pool/RuleIndex',
|
||||
meta: {
|
||||
title: '公海池规则',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:poolRule:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM3000000000000000000000000040',
|
||||
name: 'CrmSupplierHazard',
|
||||
path: 'supplier-hazard',
|
||||
component: '/crm/supplier-hazard/index',
|
||||
meta: { title: '主体危化品能力', keepAlive: true, menuCode: 'crm:supplierHazard:list' }
|
||||
},
|
||||
{
|
||||
id: '10000000000000000000000000000008',
|
||||
name: 'UserDetail',
|
||||
path: 'user/detail',
|
||||
component: '/system/user/detail',
|
||||
meta: { title: '用户详情', isHide: true, keepAlive: true }
|
||||
title: '临时存放',
|
||||
keepAlive: false
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'SYS_APPROVER_SETTING',
|
||||
name: 'SystemApproverSetting',
|
||||
path: 'approver-setting',
|
||||
component: '/system/approver-setting/index',
|
||||
meta: {
|
||||
title: '报价、合同审批人设置',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:approverSetting'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM3000000000000000000000000040',
|
||||
name: 'CrmSupplierHazard',
|
||||
path: 'supplier-hazard',
|
||||
component: '/crm/supplier-hazard/index',
|
||||
meta: {
|
||||
title: '主体危化品能力',
|
||||
keepAlive: true,
|
||||
menuCode: 'crm:supplierHazard:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000010',
|
||||
name: 'CrmClue',
|
||||
path: 'clue',
|
||||
component: '/crm/clue/index',
|
||||
meta: { title: '线索管理', keepAlive: true, menuCode: 'crm:clue:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000011',
|
||||
name: 'CrmClueDetail',
|
||||
path: 'clue/detail/:id',
|
||||
component: '/crm/clue/detail',
|
||||
meta: {
|
||||
title: '线索详情',
|
||||
keepAlive: false,
|
||||
isHide: true,
|
||||
menuCode: 'crm:clue:query'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000012',
|
||||
name: 'CrmClueStatistics',
|
||||
path: 'clue/statistics',
|
||||
component: '/crm/clue/statistics/index',
|
||||
meta: {
|
||||
title: '线索统计',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:clue:statistics'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000010',
|
||||
name: 'CrmOpportunity',
|
||||
path: 'opportunity',
|
||||
component: '/crm/opportunity/index',
|
||||
meta: { title: '商机管理', keepAlive: true, menuCode: 'crm:opportunity:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000011',
|
||||
name: 'CrmOpportunityDetail',
|
||||
path: 'opportunity/detail/:id',
|
||||
component: '/crm/opportunity/detail',
|
||||
meta: {
|
||||
title: '商机详情',
|
||||
keepAlive: false,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:query'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000012',
|
||||
name: 'CrmOpportunityKanban',
|
||||
path: 'opportunity/kanban',
|
||||
component: '/crm/opportunity/kanban',
|
||||
meta: {
|
||||
title: '商机看板',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000013',
|
||||
name: 'CrmOpportunityFunnel',
|
||||
path: 'opportunity/funnel',
|
||||
component: '/crm/opportunity/funnel',
|
||||
meta: {
|
||||
title: '销售漏斗',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM2000000000000000000000000014',
|
||||
name: 'CrmOpportunityStatistics',
|
||||
path: 'opportunity/statistics',
|
||||
component: '/crm/opportunity/statistics',
|
||||
meta: {
|
||||
title: '商机统计',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:opportunity:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000003',
|
||||
name: 'CrmCustomerPool',
|
||||
path: 'customer-pool',
|
||||
component: '/crm/customer/pool/index',
|
||||
meta: { title: '公海池', keepAlive: true, menuCode: 'crm:customerPool:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000004',
|
||||
name: 'CrmPoolRule',
|
||||
path: 'pool-rule',
|
||||
component: '/crm/customer/pool/RuleIndex',
|
||||
meta: {
|
||||
title: '公海池规则',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
menuCode: 'crm:poolRule:list'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export const useWorktabStore = defineStore(
|
||||
|
||||
/**
|
||||
* 过滤不应展示在 worktab 中的父级占位路由。
|
||||
* `/system` 仅用于承载与重定向,真实标签页为其子路由(如 `/system/tenant`)。
|
||||
* `/system` 仅用于承载与重定向,真实标签页为其子路由(如 `/system/info/tenant`)。
|
||||
*/
|
||||
const shouldIgnoreTab = (tab: Partial<WorkTab>): boolean => {
|
||||
if (tab.path === '/system') {
|
||||
|
||||
@@ -58,6 +58,8 @@ export const SIDEBAR_MENU_ICON_MAP: Record<string, string> = {
|
||||
|
||||
// ── 系统 ──
|
||||
SystemManage: 'ri:settings-3-line',
|
||||
SystemInfo: 'ri:information-line',
|
||||
SystemTemp: 'ri:inbox-archive-line',
|
||||
Tenant: 'ri:building-line',
|
||||
Dict: 'ri:book-2-line',
|
||||
Menu: 'ri:menu-line',
|
||||
|
||||
@@ -11,11 +11,25 @@ export interface PortalHomeCardItem {
|
||||
menuCode?: string
|
||||
}
|
||||
|
||||
/** 一级模块下按二级菜单分组的一行 */
|
||||
export interface PortalHomeGroup {
|
||||
/** 二级菜单 path;叶子直属合并组用空串 */
|
||||
groupPath: string
|
||||
/** 二级菜单标题;空串表示不展示分组标题(一级下的叶子页) */
|
||||
groupTitle: string
|
||||
cards: PortalHomeCardItem[]
|
||||
/** 为 true 时不渲染二级标题(CRM 等扁平二级叶子) */
|
||||
hideTitle?: boolean
|
||||
}
|
||||
|
||||
export interface PortalHomeSection {
|
||||
/** 一级模块 path */
|
||||
modulePath: string
|
||||
moduleTitle: string
|
||||
/** 扁平卡片(收藏 / 角标 / 检索共用) */
|
||||
cards: PortalHomeCardItem[]
|
||||
/** 按二级菜单分行展示 */
|
||||
groups: PortalHomeGroup[]
|
||||
}
|
||||
|
||||
/** 首页菜单目录条目(含拼音检索元数据) */
|
||||
@@ -75,40 +89,92 @@ function toCard(item: AppRouteRecord): PortalHomeCardItem | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从一级模块收集可点卡片(跳过不可导航二级标题) */
|
||||
export function collectCardsForModule(module: AppRouteRecord): PortalHomeCardItem[] {
|
||||
/** 从一级模块按二级收集分组与扁平卡片(跳过不可导航二级标题) */
|
||||
export function collectGroupsForModule(module: AppRouteRecord): {
|
||||
groups: PortalHomeGroup[]
|
||||
cards: PortalHomeCardItem[]
|
||||
} {
|
||||
const l2List = filterSidebarRoutes(module.children || [])
|
||||
const groups: PortalHomeGroup[] = []
|
||||
const cards: PortalHomeCardItem[] = []
|
||||
const seen = new Set<string>()
|
||||
/** 一级下直接挂的叶子页,按菜单顺序合并为无标题行,避免「产品管理 / 产品管理」重复 */
|
||||
let pendingFlat: PortalHomeCardItem[] | null = null
|
||||
|
||||
const push = (item: AppRouteRecord) => {
|
||||
const pushUnique = (item: AppRouteRecord): PortalHomeCardItem | null => {
|
||||
const card = toCard(item)
|
||||
if (!card || seen.has(card.path)) return
|
||||
if (!card || seen.has(card.path)) return null
|
||||
seen.add(card.path)
|
||||
cards.push(card)
|
||||
return card
|
||||
}
|
||||
|
||||
const flushFlat = () => {
|
||||
if (!pendingFlat?.length) {
|
||||
pendingFlat = null
|
||||
return
|
||||
}
|
||||
groups.push({
|
||||
groupPath: '',
|
||||
groupTitle: '',
|
||||
cards: pendingFlat,
|
||||
hideTitle: true
|
||||
})
|
||||
pendingFlat = null
|
||||
}
|
||||
|
||||
for (const l2 of l2List) {
|
||||
const links = resolveMegaPanelL3Links(l2)
|
||||
for (const link of links) {
|
||||
push(link)
|
||||
}
|
||||
const hasKids = filterSidebarRoutes(l2.children || []).length > 0
|
||||
if (hasKids && isSidebarNavigableRoute(l2)) {
|
||||
push(l2)
|
||||
const links = resolveMegaPanelL3Links(l2)
|
||||
|
||||
// 无真实子级:侧栏里的「二级」实际是一级下的叶子页(如 CRM 产品管理)
|
||||
if (!hasKids) {
|
||||
if (!pendingFlat) pendingFlat = []
|
||||
for (const link of links) {
|
||||
const card = pushUnique(link)
|
||||
if (card) pendingFlat.push(card)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
flushFlat()
|
||||
const groupCards: PortalHomeCardItem[] = []
|
||||
for (const link of links) {
|
||||
const card = pushUnique(link)
|
||||
if (card) groupCards.push(card)
|
||||
}
|
||||
if (isSidebarNavigableRoute(l2)) {
|
||||
const card = pushUnique(l2)
|
||||
if (card) groupCards.push(card)
|
||||
}
|
||||
if (!groupCards.length) continue
|
||||
groups.push({
|
||||
groupPath: String(l2.path || ''),
|
||||
groupTitle: routeTitle(l2),
|
||||
cards: groupCards
|
||||
})
|
||||
}
|
||||
return cards
|
||||
flushFlat()
|
||||
return { groups, cards }
|
||||
}
|
||||
|
||||
/** 从一级模块收集可点卡片(跳过不可导航二级标题) */
|
||||
export function collectCardsForModule(module: AppRouteRecord): PortalHomeCardItem[] {
|
||||
return collectGroupsForModule(module).cards
|
||||
}
|
||||
|
||||
export function collectPortalHomeSections(menuList: AppRouteRecord[]): PortalHomeSection[] {
|
||||
const tops = filterPortalTopModules(menuList)
|
||||
return tops
|
||||
.map((mod) => ({
|
||||
modulePath: String(mod.path || ''),
|
||||
moduleTitle: routeTitle(mod),
|
||||
cards: collectCardsForModule(mod)
|
||||
}))
|
||||
.map((mod) => {
|
||||
const { groups, cards } = collectGroupsForModule(mod)
|
||||
return {
|
||||
modulePath: String(mod.path || ''),
|
||||
moduleTitle: routeTitle(mod),
|
||||
cards,
|
||||
groups
|
||||
}
|
||||
})
|
||||
.filter((s) => s.cards.length > 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
v-for="pick in selectedInvoicePicks"
|
||||
:key="pick.key"
|
||||
class="invoice-selected-card"
|
||||
:style="{ '--invoice-group-color': invoiceGroupColor(pick.invoiceId) }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -388,7 +389,7 @@
|
||||
</span>
|
||||
<div class="invoice-selected-card__footer">
|
||||
<span class="invoice-selected-card__amount">
|
||||
{{ formatMoneyYuan(pick.amount) }} 元
|
||||
整票 {{ formatMoneyYuan(pick.amount) }} 元
|
||||
</span>
|
||||
<el-icon
|
||||
class="invoice-selected-card__download"
|
||||
@@ -761,6 +762,25 @@ const walletBalance = ref(0)
|
||||
const editingId = ref<number | null>(null)
|
||||
const titleAutoLinked = ref(true)
|
||||
|
||||
/** 同一发票共用边框色(与 BxInvoiceSelectDialog / bx-mobile 一致) */
|
||||
const INVOICE_GROUP_COLORS = [
|
||||
'#0891b2',
|
||||
'#7c3aed',
|
||||
'#d97706',
|
||||
'#db2777',
|
||||
'#2563eb',
|
||||
'#059669',
|
||||
'#ea580c',
|
||||
'#4f46e5'
|
||||
]
|
||||
|
||||
function invoiceGroupColor(invoiceId: string | number) {
|
||||
const s = String(invoiceId || '')
|
||||
let hash = 0
|
||||
for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) >>> 0
|
||||
return INVOICE_GROUP_COLORS[hash % INVOICE_GROUP_COLORS.length]
|
||||
}
|
||||
|
||||
/** 关联发票固定展示顺序:上传/创建时间从新到旧,同时间按附件 ID */
|
||||
function sortInvoicePicksByFixedOrder<T extends { createTime?: string; attachmentId?: string | number; id?: string | number; key?: string }>(
|
||||
items: T[]
|
||||
@@ -1384,7 +1404,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
.invoice-selected-card {
|
||||
position: relative;
|
||||
width: 96px;
|
||||
border: 1px solid var(--border, #d9e8e2);
|
||||
border: 2px solid var(--invoice-group-color, var(--border, #d9e8e2));
|
||||
border-radius: 6px;
|
||||
background: var(--surface, #fff);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
const detailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
|
||||
function goBack() {
|
||||
router.push({ path: '/system/clue' })
|
||||
router.push({ path: '/system/temp/clue' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -2236,7 +2236,7 @@ const handleViewCustomer = () => {
|
||||
/** 查看商机 */
|
||||
const handleViewOpportunity = () => {
|
||||
router.push({
|
||||
path: '/system/opportunity/detail',
|
||||
path: '/system/temp/opportunity/detail',
|
||||
query: { id: contract.value?.opportunityId }
|
||||
});
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
const opportunityId = ref<string>('')
|
||||
|
||||
function goBack() {
|
||||
router.push({ path: '/system/opportunity' })
|
||||
router.push({ path: '/system/temp/opportunity' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1029,7 +1029,7 @@
|
||||
/** 切换视图模式 */
|
||||
const switchView = (mode: 'list' | 'kanban') => {
|
||||
if (mode === 'kanban') {
|
||||
router.push('/system/opportunity/kanban')
|
||||
router.push('/system/temp/opportunity/kanban')
|
||||
} else {
|
||||
viewMode.value = mode
|
||||
}
|
||||
@@ -1037,7 +1037,7 @@
|
||||
|
||||
/** 跳转到漏斗分析 */
|
||||
const goToFunnel = () => {
|
||||
router.push('/system/opportunity/funnel')
|
||||
router.push('/system/temp/opportunity/funnel')
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
|
||||
@@ -185,13 +185,13 @@ const resetQuery = () => {
|
||||
}
|
||||
|
||||
const handleViewDetail = (id: number) => {
|
||||
router.push({ path: '/system/opportunity/detail', query: { id } })
|
||||
router.push({ path: '/system/temp/opportunity/detail', query: { id } })
|
||||
}
|
||||
|
||||
const handleCardAction = (command: string, opp: OpportunityVO) => {
|
||||
switch (command) {
|
||||
case 'edit':
|
||||
router.push({ path: '/system/opportunity', query: { id: opp.id, action: 'edit' } })
|
||||
router.push({ path: '/system/temp/opportunity', query: { id: opp.id, action: 'edit' } })
|
||||
break
|
||||
case 'stage':
|
||||
changeStageDialogRef.value?.open(opp.id)
|
||||
|
||||
@@ -239,7 +239,7 @@ let winRateChart: echarts.ECharts | null = null
|
||||
let ownerChart: echarts.ECharts | null = null
|
||||
|
||||
const goBack = () => {
|
||||
router.push('/system/opportunity')
|
||||
router.push('/system/temp/opportunity')
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
|
||||
@@ -78,49 +78,62 @@
|
||||
<p v-else class="portal-home-empty">暂无收藏,可在各模块菜单卡片上点星标添加</p>
|
||||
</section>
|
||||
|
||||
<section v-else class="portal-home-block">
|
||||
<div class="portal-home-grid">
|
||||
<button
|
||||
v-for="card in activeCards"
|
||||
:key="card.path"
|
||||
type="button"
|
||||
class="portal-home-card"
|
||||
@click="openCard(card.path)"
|
||||
>
|
||||
<span
|
||||
v-if="pendingCountFor(card) > 0"
|
||||
class="portal-home-card__pending"
|
||||
:aria-label="`${pendingCountFor(card)} 条待审批`"
|
||||
>
|
||||
{{ formatPendingCount(pendingCountFor(card)) }}
|
||||
</span>
|
||||
<span
|
||||
class="portal-home-card__badge"
|
||||
:data-tone="toneIndex(activeModule)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ titleAbbr(card.title) }}
|
||||
</span>
|
||||
<span class="portal-home-card__body">
|
||||
<span class="portal-home-card__title">{{ card.title }}</span>
|
||||
<span class="portal-home-card__sub">{{ panelTitle }}</span>
|
||||
</span>
|
||||
<template v-else>
|
||||
<section
|
||||
v-for="(group, gIdx) in activeGroups"
|
||||
:key="group.groupPath || `flat-${gIdx}`"
|
||||
class="portal-home-block"
|
||||
:class="{ 'portal-home-block--flat': group.hideTitle }"
|
||||
>
|
||||
<div v-if="!group.hideTitle && group.groupTitle" class="portal-home-block__head">
|
||||
<h3 class="title-bar-heading">{{ group.groupTitle }}</h3>
|
||||
<span class="portal-home-block__tip">{{ group.cards.length }} 项</span>
|
||||
</div>
|
||||
<div class="portal-home-grid">
|
||||
<button
|
||||
v-for="card in group.cards"
|
||||
:key="card.path"
|
||||
type="button"
|
||||
class="portal-home-card__star"
|
||||
:class="{ 'is-on': favoritePathSet.has(card.path) }"
|
||||
:aria-label="favoritePathSet.has(card.path) ? '取消收藏' : '加入收藏'"
|
||||
@click.stop="onToggleStar(card.path)"
|
||||
class="portal-home-card"
|
||||
@click="openCard(card.path)"
|
||||
>
|
||||
<ElIcon>
|
||||
<StarFilled v-if="favoritePathSet.has(card.path)" />
|
||||
<Star v-else />
|
||||
</ElIcon>
|
||||
<span
|
||||
v-if="pendingCountFor(card) > 0"
|
||||
class="portal-home-card__pending"
|
||||
:aria-label="`${pendingCountFor(card)} 条待审批`"
|
||||
>
|
||||
{{ formatPendingCount(pendingCountFor(card)) }}
|
||||
</span>
|
||||
<span
|
||||
class="portal-home-card__badge"
|
||||
:data-tone="toneIndex(group.groupPath || activeModule)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ titleAbbr(card.title) }}
|
||||
</span>
|
||||
<span class="portal-home-card__body">
|
||||
<span class="portal-home-card__title">{{ card.title }}</span>
|
||||
<span class="portal-home-card__sub">{{
|
||||
group.hideTitle || !group.groupTitle ? panelTitle : group.groupTitle
|
||||
}}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="portal-home-card__star"
|
||||
:class="{ 'is-on': favoritePathSet.has(card.path) }"
|
||||
:aria-label="favoritePathSet.has(card.path) ? '取消收藏' : '加入收藏'"
|
||||
@click.stop="onToggleStar(card.path)"
|
||||
>
|
||||
<ElIcon>
|
||||
<StarFilled v-if="favoritePathSet.has(card.path)" />
|
||||
<Star v-else />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!activeCards.length" class="portal-home-empty">该模块暂无可用菜单</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<p v-if="!activeGroups.length" class="portal-home-empty">该模块暂无可用菜单</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -190,19 +203,22 @@
|
||||
|
||||
const isFavView = computed(() => activeModule.value === FAV_KEY)
|
||||
|
||||
const activeCards = computed(() => {
|
||||
if (isFavView.value) return []
|
||||
const sec = modules.value.find((m) => m.modulePath === activeModule.value)
|
||||
return sec?.cards || []
|
||||
const activeSection = computed(() => {
|
||||
if (isFavView.value) return undefined
|
||||
return modules.value.find((m) => m.modulePath === activeModule.value)
|
||||
})
|
||||
|
||||
const activeGroups = computed(() => activeSection.value?.groups || [])
|
||||
|
||||
const activeCards = computed(() => activeSection.value?.cards || [])
|
||||
|
||||
const panelTitle = computed(() => {
|
||||
if (isFavView.value) return '收藏'
|
||||
return modules.value.find((m) => m.modulePath === activeModule.value)?.moduleTitle || ''
|
||||
return activeSection.value?.moduleTitle || ''
|
||||
})
|
||||
|
||||
const panelDesc = computed(() =>
|
||||
isFavView.value ? '可拖拽调整顺序,星标取消收藏' : '点击卡片进入功能,星标加入收藏'
|
||||
isFavView.value ? '可拖拽调整顺序,星标取消收藏' : '按二级菜单分组展示,点击卡片进入功能'
|
||||
)
|
||||
|
||||
const panelCount = computed(() =>
|
||||
@@ -217,6 +233,11 @@
|
||||
|
||||
const favSubTitle = (path: string) => {
|
||||
for (const sec of modules.value) {
|
||||
for (const group of sec.groups || []) {
|
||||
if (!group.cards.some((c) => c.path === path)) continue
|
||||
if (group.hideTitle || !group.groupTitle) return sec.moduleTitle
|
||||
return `${sec.moduleTitle} / ${group.groupTitle}`
|
||||
}
|
||||
if (sec.cards.some((c) => c.path === path)) return sec.moduleTitle
|
||||
}
|
||||
return '收藏入口'
|
||||
@@ -494,15 +515,23 @@
|
||||
}
|
||||
|
||||
.portal-home-block {
|
||||
margin: 0 18px 18px;
|
||||
margin: 14px 18px 8px;
|
||||
|
||||
&:last-child {
|
||||
&:last-of-type {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&--fav {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
&--flat {
|
||||
margin-top: 14px;
|
||||
|
||||
.portal-home-grid {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.portal-home-block__head {
|
||||
@@ -510,11 +539,13 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.portal-home-block__tip {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
}
|
||||
|
||||
|
||||
@@ -408,7 +408,7 @@
|
||||
|
||||
// 返回列表
|
||||
const goBack = () => {
|
||||
router.push('/system/user')
|
||||
router.push('/system/info/user')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
|
||||
Reference in new issue
Block a user