20260719215738

This commit is contained in:
oneao committed 2026-07-19 21:57:40 +08:00
1 parent 35aa108a34
commit cda7f16d3a
116 files changed
+11826 -3669

No files matched your search

-134
View File
@@ -1,134 +0,0 @@
export const mockRecords = [
{
id: "FY-2026-0001",
name: "华东区域运费结算",
state: "草稿",
customer: "华东客户中心",
businessType: "海运",
owner: "张三",
amount: "4,960.00",
currency: "CNY",
updated_at: "2026-07-16 09:20",
},
{
id: "FY-2026-0002",
name: "华南客户月度账单",
state: "处理中",
customer: "华南供应链有限公司",
businessType: "空运",
owner: "李四",
amount: "6,240.00",
currency: "CNY",
updated_at: "2026-07-16 08:45",
},
{
id: "FY-2026-0003",
name: "北方港口代理费用",
state: "已完成",
customer: "北方港口服务公司",
businessType: "陆运",
owner: "王五",
amount: "7,520.00",
currency: "USD",
updated_at: "2026-07-15 17:32",
},
{
id: "FY-2026-0004",
name: "国际空运服务费",
state: "处理中",
customer: "国际物流有限公司",
businessType: "空运",
owner: "张三",
amount: "8,800.00",
currency: "CNY",
updated_at: "2026-07-15 16:10",
},
{
id: "FY-2026-0005",
name: "出口报关服务费用",
state: "已完成",
customer: "远洋贸易有限公司",
businessType: "海运",
owner: "李四",
amount: "10,080.00",
currency: "CNY",
updated_at: "2026-07-15 14:56",
},
{
id: "FY-2026-0006",
name: "华中干线运输费用",
state: "草稿",
customer: "华中运输服务公司",
businessType: "陆运",
owner: "王五",
amount: "11,360.00",
currency: "USD",
updated_at: "2026-07-15 11:24",
},
{
id: "FY-2026-0007",
name: "仓储增值服务结算",
state: "已取消",
customer: "城市仓储有限公司",
businessType: "铁路",
owner: "张三",
amount: "12,640.00",
currency: "CNY",
updated_at: "2026-07-14 18:05",
},
{
id: "FY-2026-0008",
name: "跨境电商物流账单",
state: "已完成",
customer: "跨境电商有限公司",
businessType: "空运",
owner: "李四",
amount: "13,920.00",
currency: "CNY",
updated_at: "2026-07-14 15:40",
},
{
id: "FY-2026-0009",
name: "西南区域配送费用",
state: "处理中",
customer: "西南配送服务中心",
businessType: "陆运",
owner: "王五",
amount: "15,200.00",
currency: "USD",
updated_at: "2026-07-14 13:18",
},
{
id: "FY-2026-0010",
name: "危险品运输服务费",
state: "草稿",
customer: "特种运输有限公司",
businessType: "海运",
owner: "张三",
amount: "16,480.00",
currency: "CNY",
updated_at: "2026-07-14 10:02",
},
{
id: "FY-2026-0011",
name: "铁路联运代理费用",
state: "已完成",
customer: "铁路联运服务公司",
businessType: "铁路",
owner: "李四",
amount: "17,760.00",
currency: "CNY",
updated_at: "2026-07-13 16:27",
},
{
id: "FY-2026-0012",
name: "客户临时加急费用",
state: "已取消",
customer: "重点客户服务中心",
businessType: "空运",
owner: "王五",
amount: "19,040.00",
currency: "USD",
updated_at: "2026-07-13 09:51",
},
];
+668
View File
@@ -0,0 +1,668 @@
/**
* 模块管理 Demo 数据。
*
* 设计边界:
* - 模块配置服务于通用数据接口和标准 ModuleTable / ModuleForm。
* - 字段配置标准字段的显隐、必填、类型、字典与顺序,不定义页面整体布局。
* - 菜单由模块树中启用页面能力的模块投影,不单独维护菜单数据。
* - 权限仅用于前端菜单、路由和操作控制。
*/
export const localeOptions = [
{ label: "简体中文", value: "zh-CN" },
{ label: "English", value: "en-US" },
{ label: "繁體中文", value: "zh-TW" },
];
export const roles = [
{ id: "admin", name: "系统管理员" },
{ id: "operator", name: "业务操作员" },
{ id: "finance", name: "财务人员" },
{ id: "guest", name: "只读访客" },
];
export const fieldTypeOptions = [
{ label: "单行文本", value: "string" },
{ label: "多行文本", value: "text" },
{ label: "数字", value: "number" },
{ label: "字典选择", value: "select" },
{ label: "日期", value: "date" },
{ label: "开关", value: "boolean" },
];
export const dictionaryOptions = [
{ label: "业务类型", value: "BIZ_TYPE" },
{ label: "业务状态", value: "BIZ_STATUS" },
{ label: "用户状态", value: "USER_STATUS" },
{ label: "币种", value: "CURRENCY" },
];
export const dictionaries = {
BIZ_TYPE: [
{ value: "SEA", i18nKey: "dict.BIZ_TYPE.SEA" },
{ value: "AIR", i18nKey: "dict.BIZ_TYPE.AIR" },
],
BIZ_STATUS: [
{ value: "DRAFT", i18nKey: "dict.BIZ_STATUS.DRAFT" },
{ value: "SUBMITTED", i18nKey: "dict.BIZ_STATUS.SUBMITTED" },
{ value: "APPROVED", i18nKey: "dict.BIZ_STATUS.APPROVED" },
],
USER_STATUS: [
{ value: "ENABLED", i18nKey: "dict.USER_STATUS.ENABLED" },
{ value: "DISABLED", i18nKey: "dict.USER_STATUS.DISABLED" },
],
CURRENCY: [
{ value: "CNY", i18nKey: "dict.CURRENCY.CNY" },
{ value: "USD", i18nKey: "dict.CURRENCY.USD" },
{ value: "EUR", i18nKey: "dict.CURRENCY.EUR" },
],
};
export const modules = {
M_SYSTEM: {
code: "M_SYSTEM",
parentId: null,
sort: 10,
i18nKey: "module.M_SYSTEM",
isDirectory: true,
isPage: false,
route: "",
showInMenu: true,
querySource: "",
saveTarget: "",
primaryKey: "",
defaultOrder: "",
enabled: true,
fields: [],
actions: [],
sampleRows: [],
},
M_MODULE: {
code: "M_MODULE",
parentId: "M_SYSTEM",
sort: 10,
i18nKey: "module.M_MODULE",
isDirectory: false,
isPage: true,
route: "/demo/modules",
showInMenu: true,
querySource: "",
saveTarget: "",
primaryKey: "",
defaultOrder: "",
enabled: true,
fields: [],
actions: [{ code: "view", name: "查看" }],
sampleRows: [{}],
},
M_BUSINESS: {
code: "M_BUSINESS",
parentId: null,
sort: 20,
i18nKey: "module.M_BUSINESS",
isDirectory: true,
isPage: false,
route: "",
showInMenu: true,
querySource: "",
saveTarget: "",
primaryKey: "",
defaultOrder: "",
enabled: true,
fields: [],
actions: [],
sampleRows: [],
},
M_BIZ: {
code: "M_BIZ",
i18nKey: "module.M_BIZ",
parentId: "M_BUSINESS",
sort: 10,
isDirectory: false,
isPage: true,
route: "/business/orders",
showInMenu: true,
querySource: "v_bs_business",
saveTarget: "b_business",
primaryKey: "b_id",
defaultOrder: "b_id DESC",
enabled: true,
fields: [
{
id: "F_BIZ_NO",
code: "b_ywno",
i18nKey: "field.M_BIZ.b_ywno",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 10,
width: 150,
},
{
id: "F_BIZ_CUSTOMER",
code: "b_customer",
i18nKey: "field.M_BIZ.b_customer",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 20,
width: 160,
},
{
id: "F_BIZ_TYPE",
code: "b_type",
i18nKey: "field.M_BIZ.b_type",
dataType: "select",
dictionaryCode: "BIZ_TYPE",
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 30,
width: 110,
},
{
id: "F_BIZ_AMOUNT",
code: "b_amount",
i18nKey: "field.M_BIZ.b_amount",
dataType: "number",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: false,
queryable: true,
sort: 40,
width: 110,
},
{
id: "F_BIZ_STATUS",
code: "b_state",
i18nKey: "field.M_BIZ.b_state",
dataType: "select",
dictionaryCode: "BIZ_STATUS",
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 50,
width: 110,
},
{
id: "F_BIZ_REMARK",
code: "b_remark",
i18nKey: "field.M_BIZ.b_remark",
dataType: "text",
dictionaryCode: null,
showInList: false,
showInForm: true,
required: false,
queryable: false,
sort: 60,
width: 180,
},
],
actions: [
{ code: "view", name: "查看" },
{ code: "create", name: "新增" },
{ code: "update", name: "修改" },
{ code: "delete", name: "删除" },
{ code: "submit", name: "提交" },
{ code: "audit", name: "审核" },
{ code: "export", name: "导出" },
],
sampleRows: [
{
b_id: "10001",
b_ywno: "SE-2026-0719",
b_customer: "远海贸易",
b_type: "SEA",
b_amount: 128600,
b_state: "SUBMITTED",
b_remark: "等待审核",
},
{
b_id: "10002",
b_ywno: "AE-2026-0720",
b_customer: "华东供应链",
b_type: "AIR",
b_amount: 35600,
b_state: "DRAFT",
b_remark: "",
},
],
},
M_USER: {
code: "M_USER",
i18nKey: "module.M_USER",
parentId: "M_SYSTEM",
sort: 20,
isDirectory: false,
isPage: true,
route: "/system/users",
showInMenu: true,
querySource: "b_user",
saveTarget: "b_user",
primaryKey: "b_id",
defaultOrder: "b_userid ASC",
enabled: true,
fields: [
{
id: "F_USER_ACCOUNT",
code: "b_userid",
i18nKey: "field.M_USER.b_userid",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 10,
width: 140,
},
{
id: "F_USER_NAME",
code: "b_name",
i18nKey: "field.M_USER.b_name",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 20,
width: 120,
},
{
id: "F_USER_DEPT",
code: "b_dept",
i18nKey: "field.M_USER.b_dept",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: false,
queryable: true,
sort: 30,
width: 150,
},
{
id: "F_USER_STATUS",
code: "b_canuse",
i18nKey: "field.M_USER.b_canuse",
dataType: "select",
dictionaryCode: "USER_STATUS",
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 40,
width: 100,
},
],
actions: [
{ code: "view", name: "查看" },
{ code: "create", name: "新增" },
{ code: "update", name: "修改" },
{ code: "disable", name: "禁用" },
{ code: "reset-password", name: "重置密码" },
],
sampleRows: [
{ b_id: "U001", b_userid: "zhangsan", b_name: "张三", b_dept: "操作部", b_canuse: "ENABLED" },
{ b_id: "U002", b_userid: "lisi", b_name: "李四", b_dept: "财务部", b_canuse: "DISABLED" },
],
},
M_BIZ_QUERY: {
code: "M_BIZ_QUERY",
parentId: "M_BUSINESS",
sort: 20,
i18nKey: "module.M_BIZ_QUERY",
isDirectory: false,
isPage: true,
route: "/business/query",
showInMenu: true,
querySource: "",
saveTarget: "",
primaryKey: "",
defaultOrder: "",
enabled: true,
fields: [],
actions: [
{ code: "view", name: "查看" },
{ code: "export", name: "导出" },
],
sampleRows: [{}],
},
M_BIZ_AUDIT: {
code: "M_BIZ_AUDIT",
parentId: "M_BUSINESS",
sort: 30,
i18nKey: "module.M_BIZ_AUDIT",
isDirectory: false,
isPage: true,
route: "/business/audit",
showInMenu: false,
querySource: "",
saveTarget: "",
primaryKey: "",
defaultOrder: "",
enabled: true,
fields: [],
actions: [
{ code: "view", name: "查看" },
{ code: "audit", name: "审核" },
],
sampleRows: [{}],
},
M_BIZ_FEE: {
code: "M_BIZ_FEE",
parentId: "M_BIZ",
sort: 10,
i18nKey: "module.M_BIZ_FEE",
isDirectory: false,
isPage: false,
route: "",
showInMenu: false,
querySource: "v_bs_business_fee",
saveTarget: "b_business_fee",
primaryKey: "b_id",
defaultOrder: "b_id ASC",
enabled: true,
fields: [
{
id: "F_BIZ_FEE_AMOUNT",
code: "b_amount",
i18nKey: "field.M_BIZ_FEE.b_amount",
dataType: "number",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 10,
width: 110,
},
{
id: "F_BIZ_FEE_CURRENCY",
code: "b_currency",
i18nKey: "field.M_BIZ_FEE.b_currency",
dataType: "select",
dictionaryCode: "CURRENCY",
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 20,
width: 100,
},
],
actions: [
{ code: "view", name: "查看" },
{ code: "create", name: "新增" },
{ code: "update", name: "修改" },
{ code: "delete", name: "删除" },
],
sampleRows: [{ b_id: "BF01", b_amount: 6800, b_currency: "CNY" }],
},
M_BIZ_ATTACHMENT: {
code: "M_BIZ_ATTACHMENT",
parentId: "M_BIZ",
sort: 20,
i18nKey: "module.M_BIZ_ATTACHMENT",
isDirectory: false,
isPage: false,
route: "",
showInMenu: false,
querySource: "b_business_attachment",
saveTarget: "b_business_attachment",
primaryKey: "b_id",
defaultOrder: "b_id DESC",
enabled: true,
fields: [
{
id: "F_ATTACHMENT_NAME",
code: "b_filename",
i18nKey: "field.M_BIZ_ATTACHMENT.b_filename",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 10,
width: 180,
},
{
id: "F_ATTACHMENT_REMARK",
code: "b_remark",
i18nKey: "field.M_BIZ_ATTACHMENT.b_remark",
dataType: "text",
dictionaryCode: null,
showInList: false,
showInForm: true,
required: false,
queryable: false,
sort: 20,
width: 180,
},
],
actions: [
{ code: "view", name: "查看" },
{ code: "upload", name: "上传" },
{ code: "delete", name: "删除" },
],
sampleRows: [{ b_id: "A01", b_filename: "booking.pdf", b_remark: "订舱附件" }],
},
M_FINANCE: {
code: "M_FINANCE",
parentId: null,
sort: 30,
i18nKey: "module.M_FINANCE",
isDirectory: true,
isPage: false,
route: "",
showInMenu: true,
querySource: "",
saveTarget: "",
primaryKey: "",
defaultOrder: "",
enabled: true,
fields: [],
actions: [],
sampleRows: [],
},
M_FEE: {
code: "M_FEE",
i18nKey: "module.M_FEE",
parentId: "M_FINANCE",
sort: 10,
isDirectory: false,
isPage: true,
route: "/finance/receivables",
showInMenu: true,
querySource: "v_fee_receivable",
saveTarget: "b_fee_receivable",
primaryKey: "b_id",
defaultOrder: "b_id DESC",
enabled: true,
fields: [
{
id: "F_FEE_NO",
code: "b_feeno",
i18nKey: "field.M_FEE.b_feeno",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 10,
width: 150,
},
{
id: "F_FEE_BIZ_NO",
code: "b_ywno",
i18nKey: "field.M_FEE.b_ywno",
dataType: "string",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 20,
width: 150,
},
{
id: "F_FEE_AMOUNT",
code: "b_amount",
i18nKey: "field.M_FEE.b_amount",
dataType: "number",
dictionaryCode: null,
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 30,
width: 110,
},
{
id: "F_FEE_CURRENCY",
code: "b_currency",
i18nKey: "field.M_FEE.b_currency",
dataType: "select",
dictionaryCode: "CURRENCY",
showInList: true,
showInForm: true,
required: true,
queryable: true,
sort: 40,
width: 90,
},
],
actions: [
{ code: "view", name: "查看" },
{ code: "create", name: "新增" },
{ code: "write-off", name: "核销" },
{ code: "export", name: "导出" },
],
sampleRows: [
{
b_id: "F001",
b_feeno: "AR-260719-01",
b_ywno: "SE-2026-0719",
b_amount: 128600,
b_currency: "CNY",
},
{
b_id: "F002",
b_feeno: "AR-260719-02",
b_ywno: "AE-2026-0720",
b_amount: 5200,
b_currency: "USD",
},
],
},
};
export const rolePermissions = {
admin: [
...Object.values(modules).flatMap((module) =>
module.actions.map((action) => `module.${module.code}.${action.code}`),
),
],
operator: [
"module.M_BIZ.view",
"module.M_BIZ.create",
"module.M_BIZ.update",
"module.M_BIZ.submit",
"module.M_BIZ.export",
"module.M_BIZ_QUERY.view",
"module.M_BIZ_QUERY.export",
"module.M_BIZ_FEE.view",
"module.M_BIZ_FEE.create",
"module.M_BIZ_FEE.update",
"module.M_BIZ_ATTACHMENT.view",
"module.M_BIZ_ATTACHMENT.upload",
"module.M_FEE.view",
],
finance: [
"module.M_BIZ_QUERY.view",
"module.M_BIZ_QUERY.export",
"module.M_FEE.view",
"module.M_FEE.create",
"module.M_FEE.write-off",
"module.M_FEE.export",
],
guest: ["module.M_BIZ_QUERY.view"],
};
export const i18nData = {
"module.M_SYSTEM": { "zh-CN": "系统管理", "en-US": "System", "zh-TW": "系統管理" },
"module.M_MODULE": { "zh-CN": "模块管理", "en-US": "Module Management", "zh-TW": "模組管理" },
"module.M_USER": { "zh-CN": "用户管理", "en-US": "Users", "zh-TW": "用戶管理" },
"module.M_BUSINESS": { "zh-CN": "业务管理", "en-US": "Operations", "zh-TW": "業務管理" },
"module.M_BIZ": { "zh-CN": "海运业务单", "en-US": "Sea Orders", "zh-TW": "海運業務單" },
"module.M_BIZ_QUERY": { "zh-CN": "业务单查询", "en-US": "Order Query", "zh-TW": "業務單查詢" },
"module.M_BIZ_AUDIT": { "zh-CN": "业务单审核", "en-US": "Order Audit", "zh-TW": "業務單審核" },
"module.M_BIZ_FEE": { "zh-CN": "业务单费用", "en-US": "Order Fees", "zh-TW": "業務單費用" },
"module.M_BIZ_ATTACHMENT": {
"zh-CN": "业务单附件",
"en-US": "Order Attachments",
"zh-TW": "業務單附件",
},
"module.M_FINANCE": { "zh-CN": "财务中心", "en-US": "Finance", "zh-TW": "財務中心" },
"module.M_FEE": { "zh-CN": "应收费用", "en-US": "Receivable Fee", "zh-TW": "應收費用" },
"field.M_BIZ.b_ywno": { "zh-CN": "业务单号", "en-US": "Order No.", "zh-TW": "業務單號" },
"field.M_BIZ.b_customer": { "zh-CN": "客户", "en-US": "Customer", "zh-TW": "客戶" },
"field.M_BIZ.b_type": { "zh-CN": "业务类型", "en-US": "Business Type", "zh-TW": "業務類型" },
"field.M_BIZ.b_amount": { "zh-CN": "金额", "en-US": "Amount", "zh-TW": "金額" },
"field.M_BIZ.b_state": { "zh-CN": "状态", "en-US": "Status", "zh-TW": "狀態" },
"field.M_BIZ.b_remark": { "zh-CN": "备注", "en-US": "Remark", "zh-TW": "備註" },
"field.M_BIZ_FEE.b_amount": { "zh-CN": "费用金额", "en-US": "Fee Amount", "zh-TW": "費用金額" },
"field.M_BIZ_FEE.b_currency": { "zh-CN": "币种", "en-US": "Currency", "zh-TW": "幣別" },
"field.M_BIZ_ATTACHMENT.b_filename": {
"zh-CN": "文件名",
"en-US": "File Name",
"zh-TW": "檔案名稱",
},
"field.M_BIZ_ATTACHMENT.b_remark": {
"zh-CN": "附件说明",
"en-US": "Description",
"zh-TW": "附件說明",
},
"field.M_USER.b_userid": { "zh-CN": "账号", "en-US": "Account", "zh-TW": "帳號" },
"field.M_USER.b_name": { "zh-CN": "姓名", "en-US": "Name", "zh-TW": "姓名" },
"field.M_USER.b_dept": { "zh-CN": "部门", "en-US": "Department", "zh-TW": "部門" },
"field.M_USER.b_canuse": { "zh-CN": "状态", "en-US": "Status", "zh-TW": "狀態" },
"field.M_FEE.b_feeno": { "zh-CN": "费用单号", "en-US": "Fee No.", "zh-TW": "費用單號" },
"field.M_FEE.b_ywno": { "zh-CN": "业务单号", "en-US": "Order No.", "zh-TW": "業務單號" },
"field.M_FEE.b_amount": { "zh-CN": "金额", "en-US": "Amount", "zh-TW": "金額" },
"field.M_FEE.b_currency": { "zh-CN": "币种", "en-US": "Currency", "zh-TW": "幣別" },
"dict.BIZ_TYPE.SEA": { "zh-CN": "海运", "en-US": "Sea", "zh-TW": "海運" },
"dict.BIZ_TYPE.AIR": { "zh-CN": "空运", "en-US": "Air", "zh-TW": "空運" },
"dict.BIZ_STATUS.DRAFT": { "zh-CN": "草稿", "en-US": "Draft", "zh-TW": "草稿" },
"dict.BIZ_STATUS.SUBMITTED": { "zh-CN": "已提交", "en-US": "Submitted", "zh-TW": "已提交" },
"dict.BIZ_STATUS.APPROVED": { "zh-CN": "已审核", "en-US": "Approved", "zh-TW": "已審核" },
"dict.USER_STATUS.ENABLED": { "zh-CN": "启用", "en-US": "Enabled", "zh-TW": "啟用" },
"dict.USER_STATUS.DISABLED": { "zh-CN": "禁用", "en-US": "Disabled", "zh-TW": "停用" },
"dict.CURRENCY.CNY": { "zh-CN": "人民币", "en-US": "CNY", "zh-TW": "人民幣" },
"dict.CURRENCY.USD": { "zh-CN": "美元", "en-US": "USD", "zh-TW": "美元" },
"dict.CURRENCY.EUR": { "zh-CN": "欧元", "en-US": "EUR", "zh-TW": "歐元" },
};
@@ -36,15 +36,18 @@ import AppToolbar from "./components/AppToolbar.vue";
}
.app-main {
display: flex;
height: 100%;
min-width: 0;
min-height: 0;
flex-direction: column;
overflow: hidden;
background: var(--fms-bg);
}
.app-content {
width: 100%;
flex: 1 1 auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
@@ -20,8 +20,9 @@ const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
const menuItems = [
const fallbackMenuItems = [
{
key: "/dashboard",
icon: menuIcon(House),
@@ -139,8 +140,8 @@ const menuItems = [
{ key: "/system/field", label: "字段配置" },
{ key: "/system/statisticalSettings", label: "查询设置" },
{ key: "/system/mappingConfig", label: "映射配置" },
{ key: "/system/moduleSettings", label: "模块设置" },
{ key: "/system/autoId", label: "自动编码" },
{ key: "/demo/modules", label: "模块管理" },
],
},
{
@@ -160,20 +161,30 @@ const menuItems = [
},
];
const menuItems = ref(fallbackMenuItems);
const selectedKeys = computed(() => [route.meta.menuKey || route.path]);
const openKeys = ref([]);
const parentKeyByRoute = new Map(
menuItems.flatMap((item) =>
item.children?.map((child) => [child.key, item.key]) ?? [],
),
);
const parentKeysByRoute = computed(() => {
const result = new Map();
const visit = (items, parents = []) => {
items.forEach((item) => {
if (item.children?.length) visit(item.children, [...parents, item.key]);
else result.set(item.key, parents);
});
};
visit(menuItems.value);
return result;
});
watch(
() => route.meta.menuKey || route.path,
(menuKey) => {
const parentKey = parentKeyByRoute.get(menuKey);
if (parentKey && !openKeys.value.includes(parentKey)) {
openKeys.value = [...openKeys.value, parentKey];
[() => route.meta.menuKey || route.path, menuItems],
([menuKey]) => {
const parentKeys = parentKeysByRoute.value.get(menuKey) || [];
const nextKeys = new Set(openKeys.value);
parentKeys.forEach((key) => nextKeys.add(key));
if (nextKeys.size !== openKeys.value.length) {
openKeys.value = Array.from(nextKeys);
}
},
{ immediate: true },
@@ -193,16 +204,16 @@ function updateOpenKeys(keys) {
<template>
<a-layout-sider
class="app-sider"
:collapsed="appStore.sidebarCollapsed"
:collapsed="effectiveCollapsed"
:trigger="null"
:collapsed-width="56"
collapsible
theme="light"
width="208"
>
<div class="brand" :class="{ 'brand--collapsed': appStore.sidebarCollapsed }">
<div class="brand" :class="{ 'brand--collapsed': effectiveCollapsed }">
<div class="brand__mark">F</div>
<div class="brand__copy" :aria-hidden="appStore.sidebarCollapsed">
<div class="brand__copy" :aria-hidden="effectiveCollapsed">
<strong>FMS 管理系统</strong>
</div>
</div>
@@ -219,7 +230,7 @@ function updateOpenKeys(keys) {
</div>
<div class="sider-footer">
<a-tooltip :title="appStore.sidebarCollapsed ? '展开侧栏' : '收起侧栏'" placement="right">
<a-tooltip :title="effectiveCollapsed ? '展开侧栏' : '收起侧栏'" placement="right">
<a-button
type="text"
class="sider-collapse"
@@ -227,7 +238,7 @@ function updateOpenKeys(keys) {
@click="appStore.toggleSidebar"
>
<template #icon>
<PanelLeftOpen v-if="appStore.sidebarCollapsed" />
<PanelLeftOpen v-if="effectiveCollapsed" />
<PanelLeftClose v-else />
</template>
</a-button>
+19 -1
View File
@@ -36,6 +36,7 @@ const routes = [
section: "业务管理",
menuKey: "/business/orders",
source: "v_bs_business",
orderBy: "b_id DESC",
},
},
{
@@ -48,6 +49,7 @@ const routes = [
section: "业务管理",
menuKey: "/business/orders",
listPath: "/business/orders",
source: "v_bs_business",
detail: true,
},
},
@@ -60,6 +62,7 @@ const routes = [
section: "费用管理",
menuKey: "/fees/receivable",
source: "v_fee_receivable",
orderBy: "b_id DESC",
},
},
{
@@ -72,6 +75,7 @@ const routes = [
section: "费用管理",
menuKey: "/fees/receivable",
listPath: "/fees/receivable",
source: "v_fee_receivable",
detail: true,
},
},
@@ -84,6 +88,7 @@ const routes = [
section: "报表中心",
menuKey: "/reports/operations",
source: "v_report_operations",
orderBy: "b_id DESC",
},
},
{
@@ -96,6 +101,7 @@ const routes = [
section: "报表中心",
menuKey: "/reports/operations",
listPath: "/reports/operations",
source: "v_report_operations",
detail: true,
},
},
@@ -107,7 +113,8 @@ const routes = [
title: "用户管理",
section: "系统设置",
menuKey: "/system/users",
source: "sys_user",
source: "b_user",
orderBy: "b_id ASC",
},
},
{
@@ -120,9 +127,20 @@ const routes = [
section: "系统设置",
menuKey: "/system/users",
listPath: "/system/users",
source: "b_user",
detail: true,
},
},
{
path: "demo/modules",
name: "demo-modules",
component: () => import("../views/ModuleManageDemo.vue"),
meta: {
title: "模块管理",
section: "系统管理",
menuKey: "/demo/modules",
},
},
],
},
{
+62 -7
View File
@@ -1,11 +1,20 @@
import { post } from "./http";
import { get, post } from "./http";
/**
* @template T
* @typedef {object} ApiResult
* @property {boolean} success 前端根据 code 补充的成功标记。
* @property {number} code 后端业务状态码,0 表示成功。
* @property {string} message 响应消息。
* @property {T} data 响应数据。
*/
/**
* 机构用户登录。
* @param {string} orgId 机构码。
* @param {string} userId 用户 ID。
* @param {string} password 密码。
* @returns {Promise<{success: boolean, code: number, message: string, data: {token: string, orgid: string, user: {id: string, name: string}}}>} 登录响应。
* @returns {Promise<ApiResult<{token: string, orgid: string, user: {id: string, name: string}}>>} 登录响应。
*/
export function loginApi(orgId, userId, password) {
return post("/auth/login", {
@@ -19,15 +28,15 @@ export function loginApi(orgId, userId, password) {
* 按表或视图加载数据。
* @param {string} viewName 表名或视图名,必填。
* @param {string} [searchCondition] 查询条件,不包含 WHERE。
* @param {string} [orderField] 排序字段,可带 ASC 或 DESC。
* @param {string} [orderBy] 排序字段,可带 ASC 或 DESC。
* @param {string[]} [searchColumns] 需要返回的字段列表,省略时返回全部字段。
* @returns {Promise<{success: boolean, code: number, message: string, data: object[]}>} 查询响应。
* @returns {Promise<ApiResult<object[]>>} 查询响应。
*/
export function loadDataApi(viewName, searchCondition, orderField, searchColumns) {
export function loadDataApi(viewName, searchCondition, orderBy, searchColumns) {
return post("/data/loaddata", {
view_name: viewName,
search_condition: searchCondition,
order_field: orderField,
order_by: orderBy,
search_columns: searchColumns,
});
}
@@ -35,8 +44,54 @@ export function loadDataApi(viewName, searchCondition, orderField, searchColumns
/**
* 执行只读 SQL 查询。
* @param {string} sql 完整的 SELECT 或 WITH 查询语句。
* @returns {Promise<{success: boolean, code: number, message: string, data: object[]}>} 查询响应。
* @returns {Promise<ApiResult<object[]>>} 查询响应。
*/
export function loadDataBySqlApi(sql) {
return post("/data/loaddatabysql", { sql });
}
/**
* 分页加载表或视图数据。
* @param {object} options 分页查询参数。
* @param {string} options.viewName 表名或视图名,必填。
* @param {string} options.orderBy 排序字段,必填,可用逗号分隔多个字段。
* @param {string[]} [options.searchColumns] 需要返回的字段列表。
* @param {string} [options.searchCondition] 查询条件,不包含 WHERE。
* @param {number} [options.pageNo=1] 页码,从 1 开始。
* @param {number} [options.pageSize=20] 每页记录数。
* @returns {Promise<ApiResult<{rows: object[], row_count: number, total: number, page_no: number, page_size: number}>>} 分页响应。
*/
export function pageDataApi({
viewName,
orderBy,
searchColumns,
searchCondition,
pageNo = 1,
pageSize = 20,
}) {
return post("/data/page", {
view_name: viewName,
search_columns: searchColumns,
search_condition: searchCondition,
order_by: orderBy,
page_no: pageNo,
page_size: pageSize,
});
}
/**
* 在同一事务中保存一个或多个表的数据变更。
* @param {{table: string, key_field: string, inserts?: object[], updates?: object[], deletes?: object[]}[]} requests 保存配置,不能为空数组。
* @returns {Promise<ApiResult<null>>} 保存响应。
*/
export function saveObjectApi(requests) {
return post("/data/saveobjt", requests);
}
/**
* 获取下一个雪花 ID。
* @returns {Promise<ApiResult<string>>} 取号响应,雪花 ID 使用字符串避免精度丢失。
*/
export function nextIdApi() {
return get("/data/nextid");
}
+4
View File
@@ -87,4 +87,8 @@ export function post(path, data, config) {
return http.post(path, data, config);
}
export function get(path, config) {
return http.get(path, config);
}
export default http;
+47 -20
View File
@@ -2,7 +2,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Ellipsis, ListFilter, Plus, Search } from "@lucide/vue";
import { loadDataApi } from "../services/api";
import { loadDataApi, pageDataApi } from "../services/api";
const route = useRoute();
const router = useRouter();
@@ -53,6 +53,9 @@ const advancedFilters = reactive(createEmptyAdvancedFilters());
const draftFilterCount = computed(
() => Object.values(advancedFilters).filter(isFilterValueFilled).length,
);
const hasActiveFilters = computed(
() => Boolean(filters.keyword.trim()) || appliedFilterCount.value > 0,
);
function createEmptyAdvancedFilters() {
return {
@@ -89,9 +92,22 @@ function getRowValue(row, candidates) {
function normalizeRow(row, index) {
const id = getRowValue(row, ["id", "b_id", "user_id", "userid"]);
const name = getRowValue(row, ["name", "b_name", "customer_name", "title"]);
const state = getRowValue(row, ["state", "b_state", "status"]);
const updatedAt = getRowValue(row, ["updated_at", "update_time", "updated_time", "b_updated_at"]);
const name = getRowValue(row, [
"name",
"b_name",
"customer_name",
"v_customer_company",
"b_ywno",
"title",
]);
const state = getRowValue(row, ["state", "b_state", "status", "b_canuse"]);
const updatedAt = getRowValue(row, [
"updated_at",
"b_updatedatetime",
"update_time",
"updated_time",
"b_updated_at",
]);
return {
...row,
@@ -176,7 +192,7 @@ function applyLocalFilters() {
function searchData() {
pagination.current = 1;
applyLocalFilters();
loadData();
}
async function loadData() {
@@ -188,11 +204,27 @@ async function loadData() {
loadError.value = "";
try {
const response = await loadDataApi(viewName);
const result = response.data;
const response = hasActiveFilters.value
? await loadDataApi(viewName)
: await pageDataApi({
viewName,
orderBy: String(route.meta.orderBy || "b_id DESC"),
pageNo: pagination.current,
pageSize: pagination.pageSize,
});
if (currentSequence !== loadSequence) return;
sourceRows.value = (Array.isArray(result) ? result : []).map(normalizeRow);
applyLocalFilters();
if (hasActiveFilters.value) {
sourceRows.value = (Array.isArray(response.data) ? response.data : []).map(normalizeRow);
applyLocalFilters();
} else {
const result = response.data ?? {};
sourceRows.value = [];
rows.value = (Array.isArray(result.rows) ? result.rows : []).map(normalizeRow);
total.value = Number(result.total) || 0;
pagination.current = Number(result.page_no) || pagination.current;
pagination.pageSize = Number(result.page_size) || pagination.pageSize;
}
} catch (error) {
if (currentSequence !== loadSequence) return;
sourceRows.value = [];
@@ -211,7 +243,11 @@ function openDetail(record) {
function changePage(page, pageSize) {
pagination.current = page;
pagination.pageSize = pageSize;
applyLocalFilters();
if (hasActiveFilters.value) {
applyLocalFilters();
} else {
loadData();
}
}
function resetAdvancedFilters() {
@@ -222,7 +258,7 @@ function applyAdvancedFilters() {
pagination.current = 1;
appliedFilterCount.value = draftFilterCount.value;
advancedOpen.value = false;
applyLocalFilters();
loadData();
}
function updateTableHeight() {
@@ -593,13 +629,4 @@ watch(
max-width: 100vw;
}
@media (max-width: 520px) {
.advanced-filter-form {
grid-template-columns: minmax(0, 1fr);
}
.filter-span-full {
grid-column: 1;
}
}
</style>
+80 -26
View File
@@ -1,33 +1,79 @@
<script setup>
import { computed, ref } from "vue";
import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ArrowLeft } from "@lucide/vue";
import { mockRecords } from "../data/mockRecords";
import { loadDataApi } from "../services/api";
const route = useRoute();
const router = useRouter();
const remark = ref("");
const loading = ref(false);
const loadError = ref("");
const sourceRecord = ref(null);
let loadSequence = 0;
const recordId = computed(() => String(route.params.id ?? ""));
const detailTitle = computed(() => route.meta.listTitle || route.meta.title || "详情");
const record = computed(
() =>
mockRecords.find((item) => item.id === recordId.value) ?? {
id: recordId.value,
name: `${detailTitle.value} ${recordId.value}`,
state: "草稿",
businessType: "未设置",
owner: "未设置",
customer: "未设置",
amount: "0.00",
currency: "CNY",
updated_at: "--",
},
);
function getRowValue(row, candidates) {
if (!row) return undefined;
const keys = Object.keys(row);
for (const candidate of candidates) {
const matchedKey = keys.find((key) => key.toLowerCase() === candidate.toLowerCase());
if (matchedKey !== undefined && row[matchedKey] != null) return row[matchedKey];
}
return undefined;
}
const record = computed(() => {
const row = sourceRecord.value;
return {
id: recordId.value,
name: getRowValue(row, ["name", "b_name", "v_customer_company", "b_ywno", "title"]) ?? "--",
state: getRowValue(row, ["state", "b_state", "status", "b_canuse"]) ?? "--",
businessType: getRowValue(row, ["business_type", "b_ywtype", "b_type"]) ?? "--",
owner: getRowValue(row, ["owner", "owner_name", "v_sales_name", "b_owner_id"]) ?? "--",
customer: getRowValue(row, ["customer", "customer_name", "v_customer_company"]) ?? "--",
amount: getRowValue(row, ["amount", "total_amount", "b_amount"]) ?? "--",
currency: getRowValue(row, ["currency", "currency_code", "b_currency"]) ?? "",
updated_at:
getRowValue(row, ["updated_at", "b_updatedatetime", "update_time", "updated_time"]) ?? "--",
};
});
function escapeSqlString(value) {
return String(value).replaceAll("'", "''");
}
async function loadDetail() {
const currentSequence = ++loadSequence;
const source = String(route.meta.source || "");
if (!source || !recordId.value) {
sourceRecord.value = null;
return;
}
loading.value = true;
loadError.value = "";
sourceRecord.value = null;
try {
const response = await loadDataApi(source, `b_id = N'${escapeSqlString(recordId.value)}'`);
if (currentSequence !== loadSequence) return;
sourceRecord.value = Array.isArray(response.data) ? (response.data[0] ?? null) : null;
if (!sourceRecord.value) loadError.value = "未找到对应记录";
} catch (error) {
if (currentSequence !== loadSequence) return;
loadError.value = error.message || "详情加载失败";
} finally {
if (currentSequence === loadSequence) loading.value = false;
}
}
function backToList() {
router.push(String(route.meta.listPath || "/dashboard"));
}
watch([() => route.meta.source, recordId], loadDetail, { immediate: true });
</script>
<template>
@@ -44,16 +90,24 @@ function backToList() {
</header>
<main class="detail-content">
<a-descriptions bordered size="small" :column="{ xs: 1, sm: 2, md: 3 }">
<a-descriptions-item label="编号">{{ recordId }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ record.name }}</a-descriptions-item>
<a-descriptions-item label="状态">{{ record.state }}</a-descriptions-item>
<a-descriptions-item label="客户">{{ record.customer }}</a-descriptions-item>
<a-descriptions-item label="业务类型">{{ record.businessType }}</a-descriptions-item>
<a-descriptions-item label="负责人">{{ record.owner }}</a-descriptions-item>
<a-descriptions-item label="金额">{{ record.amount }} {{ record.currency }}</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ record.updated_at }}</a-descriptions-item>
</a-descriptions>
<a-spin :spinning="loading">
<a-alert v-if="loadError" type="error" show-icon :message="loadError">
<template #action><a-button size="small" @click="loadDetail">重试</a-button></template>
</a-alert>
<a-descriptions v-else bordered size="small" :column="3">
<a-descriptions-item label="编号">{{ recordId }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ record.name }}</a-descriptions-item>
<a-descriptions-item label="状态">{{ record.state }}</a-descriptions-item>
<a-descriptions-item label="客户">{{ record.customer }}</a-descriptions-item>
<a-descriptions-item label="业务类型">{{ record.businessType }}</a-descriptions-item>
<a-descriptions-item label="负责人">{{ record.owner }}</a-descriptions-item>
<a-descriptions-item label="金额">
{{ record.amount }} {{ record.currency }}
</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ record.updated_at }}</a-descriptions-item>
</a-descriptions>
</a-spin>
<section class="detail-remark">
<div class="detail-remark__label">备注</div>
File diff suppressed because it is too large. Load diff