This commit is contained in:
hero920103 committed 2026-10-11 23:25:36 +08:00
1 parent 48f3bd997c
commit 184d5b0403
9 files changed
+513 -115

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
-- ============================================================
-- 进销存(erp)—— 菜单配置(yz_system_menu)
-- 幂等脚本:按 path 判重,重复执行不会产生重复菜单
-- 执行:mysql -h <host> -P <port> -u <user> -p <db> --default-character-set=utf8mb4 < erp_menus.sql
-- ============================================================
-- 目标结构(「大类」= 模块根下一层目录,会作为移动端功能页的分组标题;
-- 「小类」= 叶子菜单,渲染为功能卡片):
-- 工作台 -> 工作台(复用已有仪表盘页面 /apps/erp/dashboard)
-- 基础资料 -> 商品 / 供应商 / 客户 / 仓库
-- 采购管理 -> 采购订单 / 采购入库 / 采购退货
-- 销售管理 -> 销售订单 / 销售出库 / 销售退货
-- 库存管理 -> 入库 / 出库 / 调拨 / 盘点 / 库存查询
-- 财务管理 -> 应收 / 应付 / 收付款
-- 报表中心 -> 报表中心
-- 系统管理 -> 系统管理 / 账套管理(账套列表)/ 基础设置
--
-- 字段说明:
-- type 1=目录(大类),2=页面(小类)
-- views '[2]' 租户端:Web 租户后台与移动端共用同一份菜单
-- platforms 留 NULL:展示端继承 erp 模块配置
-- 未实现的页面 component_path 留空:Web 端为空白页,移动端落到「功能开发中」占位页
-- icon 完整 Font Awesome 类名;移动端必须落在 uniapp/utils/fa-icons.js 白名单内才显示
SET @erp_id := (
SELECT id FROM yz_system_menu
WHERE path = '/apps/erp' AND delete_time IS NULL
LIMIT 1
);
-- ---------- 1. 八个大类(目录) ----------
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '工作台', '/apps/erp/workbench', NULL, 'fa-solid fa-gauge', 1, 1, 1, '[2]', 1, 'system', '进销存大类:工作台'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/workbench' AND delete_time IS NULL) x);
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '基础资料', '/apps/erp/basic', NULL, 'fa-solid fa-database', 2, 1, 1, '[2]', 1, 'system', '进销存大类:基础资料'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/basic' AND delete_time IS NULL) x);
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '采购管理', '/apps/erp/purchase', NULL, 'fa-solid fa-cart-shopping', 3, 1, 1, '[2]', 1, 'system', '进销存大类:采购管理'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/purchase' AND delete_time IS NULL) x);
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '销售管理', '/apps/erp/sales', NULL, 'fa-solid fa-tags', 4, 1, 1, '[2]', 1, 'system', '进销存大类:销售管理'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/sales' AND delete_time IS NULL) x);
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '库存管理', '/apps/erp/stock', NULL, 'fa-solid fa-warehouse', 5, 1, 1, '[2]', 1, 'system', '进销存大类:库存管理'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/stock' AND delete_time IS NULL) x);
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '财务管理', '/apps/erp/finance', NULL, 'fa-solid fa-coins', 6, 1, 1, '[2]', 1, 'system', '进销存大类:财务管理'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/finance' AND delete_time IS NULL) x);
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '报表中心', '/apps/erp/report', NULL, 'fa-solid fa-chart-column', 7, 1, 1, '[2]', 1, 'system', '进销存大类:报表中心'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/report' AND delete_time IS NULL) x);
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT @erp_id, '系统管理', '/apps/erp/system', NULL, 'fa-solid fa-gears', 8, 1, 1, '[2]', 1, 'system', '进销存大类:系统管理'
FROM DUAL
WHERE @erp_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/system' AND delete_time IS NULL) x);
-- 取回八个大类的ID,供小类挂载
SET @erp_workbench := (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/workbench' AND delete_time IS NULL LIMIT 1);
SET @erp_basic := (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/basic' AND delete_time IS NULL LIMIT 1);
SET @erp_system := (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/system' AND delete_time IS NULL LIMIT 1);
-- ---------- 2. 已有菜单迁入新树(避免同一功能出现两份) ----------
-- 仪表盘 -> 工作台大类,并更名为「工作台」;路由与页面文件不变
UPDATE yz_system_menu
SET pid = @erp_workbench, title = '工作台', sort = 1, update_time = NOW()
WHERE delete_time IS NULL AND path = '/apps/erp/dashboard' AND @erp_workbench IS NOT NULL;
-- 供应商列表 / 客户列表 -> 基础资料大类,去掉「列表」后缀;路由与页面文件不变
UPDATE yz_system_menu
SET pid = @erp_basic, title = '供应商', sort = 2, update_time = NOW()
WHERE delete_time IS NULL AND path = '/apps/erp/suppliers' AND @erp_basic IS NOT NULL;
UPDATE yz_system_menu
SET pid = @erp_basic, title = '客户', sort = 3, update_time = NOW()
WHERE delete_time IS NULL AND path = '/apps/erp/customers' AND @erp_basic IS NOT NULL;
-- 账套管理 / 基础设置 -> 系统管理大类
UPDATE yz_system_menu
SET pid = @erp_system, sort = 2, update_time = NOW()
WHERE delete_time IS NULL AND pid = @erp_id AND title = '账套管理' AND @erp_system IS NOT NULL;
UPDATE yz_system_menu
SET pid = @erp_system, sort = 3, update_time = NOW()
WHERE delete_time IS NULL AND pid = @erp_id AND title = '基础设置' AND @erp_system IS NOT NULL;
-- ---------- 3. 新增小类(页面) ----------
DROP TEMPORARY TABLE IF EXISTS tmp_erp_leaf;
CREATE TEMPORARY TABLE tmp_erp_leaf (
parent_path VARCHAR(200) NOT NULL,
title VARCHAR(50) NOT NULL,
path VARCHAR(200) NOT NULL,
icon VARCHAR(100) NOT NULL,
sort INT NOT NULL,
remark VARCHAR(500) NOT NULL,
PRIMARY KEY (path)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
INSERT INTO tmp_erp_leaf (parent_path, title, path, icon, sort, remark) VALUES
('/apps/erp/basic', '商品', '/apps/erp/basic/product', 'fa-solid fa-box', 1, '商品档案'),
('/apps/erp/basic', '仓库', '/apps/erp/basic/warehouse', 'fa-solid fa-boxes-packing', 4, '仓库档案'),
('/apps/erp/purchase', '采购订单', '/apps/erp/purchase/order', 'fa-solid fa-file-invoice', 1, '采购订单'),
('/apps/erp/purchase', '采购入库', '/apps/erp/purchase/inbound', 'fa-solid fa-dolly', 2, '采购入库单'),
('/apps/erp/purchase', '采购退货', '/apps/erp/purchase/return', 'fa-solid fa-rotate-left', 3, '采购退货单'),
('/apps/erp/sales', '销售订单', '/apps/erp/sales/order', 'fa-solid fa-file-signature', 1, '销售订单'),
('/apps/erp/sales', '销售出库', '/apps/erp/sales/outbound', 'fa-solid fa-truck-fast', 2, '销售出库单'),
('/apps/erp/sales', '销售退货', '/apps/erp/sales/return', 'fa-solid fa-arrow-right-arrow-left',3, '销售退货单'),
('/apps/erp/stock', '入库', '/apps/erp/stock/inbound', 'fa-solid fa-box-open', 1, '其他入库'),
('/apps/erp/stock', '出库', '/apps/erp/stock/outbound', 'fa-solid fa-truck', 2, '其他出库'),
('/apps/erp/stock', '调拨', '/apps/erp/stock/transfer', 'fa-solid fa-right-left', 3, '仓库调拨'),
('/apps/erp/stock', '盘点', '/apps/erp/stock/taking', 'fa-solid fa-clipboard-check', 4, '库存盘点'),
('/apps/erp/stock', '库存查询', '/apps/erp/stock/query', 'fa-solid fa-magnifying-glass', 5, '库存查询'),
('/apps/erp/finance', '应收', '/apps/erp/finance/receivable', 'fa-solid fa-file-invoice-dollar', 1, '应收账款'),
('/apps/erp/finance', '应付', '/apps/erp/finance/payable', 'fa-solid fa-money-check-dollar', 2, '应付账款'),
('/apps/erp/finance', '收付款', '/apps/erp/finance/payment', 'fa-solid fa-cash-register', 3, '收付款单'),
('/apps/erp/report', '报表中心', '/apps/erp/report/index', 'fa-solid fa-chart-line', 1, '进销存报表'),
('/apps/erp/system', '系统管理', '/apps/erp/system/index', 'fa-solid fa-sliders', 1, '进销存系统设置');
INSERT INTO yz_system_menu
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
SELECT p.id, t.title, t.path, NULL, t.icon, t.sort, 1, 1, '[2]', 2, 'system', t.remark
FROM tmp_erp_leaf t
JOIN yz_system_menu p ON p.path = t.parent_path AND p.delete_time IS NULL
LEFT JOIN yz_system_menu e ON e.path = t.path AND e.delete_time IS NULL
WHERE e.id IS NULL;
DROP TEMPORARY TABLE IF EXISTS tmp_erp_leaf;
-- 补图标:迁移过来的菜单原本没有图标,移动端会退化成兜底图标,这里只填空值、不覆盖已有配置
UPDATE yz_system_menu SET icon = 'fa-solid fa-store', update_time = NOW()
WHERE delete_time IS NULL AND path = '/apps/erp/suppliers' AND (icon IS NULL OR icon = '');
UPDATE yz_system_menu SET icon = 'fa-solid fa-address-book', update_time = NOW()
WHERE delete_time IS NULL AND path = '/apps/erp/customers' AND (icon IS NULL OR icon = '');
UPDATE yz_system_menu SET icon = 'fa-solid fa-book-bookmark', update_time = NOW()
WHERE delete_time IS NULL AND pid = @erp_system AND title = '账套管理' AND (icon IS NULL OR icon = '');
-- ---------- 4. 清理被搬空的两级旧目录(供应商管理 / 客户管理) ----------
-- 子菜单已迁走(供应商列表 / 客户列表),留着会变成空分组。
-- 先取出「已无子菜单」的旧目录ID,再按ID软删除:MySQL 不允许在 UPDATE 的子查询里读同一张表。
SET @erp_empty_dir := NULL;
SELECT GROUP_CONCAT(d.id) INTO @erp_empty_dir
FROM yz_system_menu d
LEFT JOIN yz_system_menu c ON c.pid = d.id AND c.delete_time IS NULL
WHERE d.pid = @erp_id
AND d.delete_time IS NULL
AND d.title IN ('供应商管理', '客户管理')
AND d.path IN ('/apps/erp/supplier', '/apps/erp/customer')
AND c.id IS NULL;
UPDATE yz_system_menu
SET delete_time = NOW(), status = 0
WHERE delete_time IS NULL AND FIND_IN_SET(id, IFNULL(@erp_empty_dir, ''));
-- ---------- 5. 结果校验(大类 + 其下小类) ----------
SELECT d.sort AS 大类序号, d.title AS 大类, IFNULL(m.title, '(无)') AS 小类, m.sort AS 小类序号,
m.path AS 路径, m.icon AS 图标, m.component_path AS 页面文件
FROM yz_system_menu d
LEFT JOIN yz_system_menu m ON m.pid = d.id AND m.delete_time IS NULL
WHERE d.pid = @erp_id AND d.delete_time IS NULL
ORDER BY d.sort, m.sort, m.id;
+12 -30
View File
@@ -26,9 +26,7 @@
<view v-else class="group-list">
<view v-for="group in groups" :key="group.id" class="group-card">
<view class="group-head">
<view class="group-bar" />
<text class="group-title">{{ group.title }}</text>
<text class="group-count">{{ group.items.length }}</text>
</view>
<view class="grid">
<view
@@ -38,8 +36,11 @@
@tap="openMenu(item, item.title)"
>
<view class="fn-card" :class="{ locked: item.locked }">
<view class="tint-icon fn-icon" :class="item.locked ? 'tint-off' : 'tint-' + (item.tintIndex % 8)">
<FaIcon :name="item.icon || fallbackIcon" color="#ffffff" :size="18" />
<view
class="tint-icon fn-icon"
:class="item.locked ? 'tint-off' : 'tint-' + (item.tintIndex % 8)"
>
<FaIcon :name="item.icon || fallbackIcon" color="#ffffff" :size="20" />
</view>
<text class="fn-name">{{ item.title }}</text>
</view>
@@ -265,7 +266,9 @@ onShow(() => {
word-break: break-all;
}
/* ============ 分组卡:外层一个 card,内含若干功能 card ============ */
/* ============ 分组卡:白色卡片 + 一行 4 个功能宫格(图标在上、名称在下) ============
cell 占 25% 宽并自带右侧间隙,容器用负 margin 抵消末列间隙,
避免使用 calc 除法——小程序端 wxss 对复杂 calc 支持不稳。 */
.group-card {
@include card;
padding: 24rpx 20rpx 8rpx;
@@ -282,32 +285,15 @@ onShow(() => {
padding: 0 4rpx 20rpx;
}
.group-bar {
width: 6rpx;
height: 28rpx;
border-radius: $radius-full;
background: $color-primary;
flex-shrink: 0;
}
.group-title {
@include section-title;
flex: 1;
min-width: 0;
font-size: 28rpx;
font-weight: 600;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.group-count {
@include chip;
}
/* ============ 功能宫格:一行 4 个 ============
cell 占 25% 宽并自带右侧间隙,容器用负 margin 抵消末列间隙,
避免使用 calc 除法——小程序端 wxss 对复杂 calc 支持不稳。 */
.grid {
display: flex;
flex-wrap: wrap;
@@ -317,15 +303,11 @@ onShow(() => {
.cell {
width: 25%;
padding-right: 16rpx;
margin-bottom: 16rpx;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.fn-card {
height: 100%;
padding: 18rpx 6rpx 16rpx;
border-radius: $radius-md;
background: $color-bg-page;
display: flex;
flex-direction: column;
align-items: center;
@@ -338,12 +320,12 @@ onShow(() => {
}
.fn-icon {
@include tint-icon(64rpx, 18rpx);
@include tint-icon(84rpx, 24rpx);
}
.fn-name {
max-width: 100%;
font-size: 22rpx;
font-size: 24rpx;
line-height: 1.3;
color: $color-text;
text-align: center;
+2 -8
View File
@@ -63,7 +63,6 @@
<view class="group-head">
<view class="group-bar" />
<text class="group-title">常用操作</text>
<text class="group-count">3</text>
</view>
<view class="grid">
<view class="cell" @tap="goFunctions">
@@ -82,14 +81,14 @@
<text class="fn-name">账号与安全</text>
</view>
</view>
<view class="cell" @tap="goBackNav">
<!-- <view class="cell" @tap="goBackNav">
<view class="fn-card">
<view class="tint-icon fn-icon tint-2">
<FaIcon name="arrow-right-arrow-left" color="#ffffff" :size="18" />
</view>
<text class="fn-name">切换子系统</text>
</view>
</view>
</view> -->
</view>
</view>
@@ -422,11 +421,6 @@ onShow(() => {
white-space: nowrap;
}
.group-count {
@include chip;
flex-shrink: 0;
}
/* cell 占 25% 并自带右间隙,容器负 margin 抵消末列间隙 */
.grid {
display: flex;
+8
View File
@@ -289,6 +289,14 @@
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/common/developing",
"style": {
"navigationBarTitleText": "功能开发中",
"navigationBarBackgroundColor": "#F8FAFC",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/profile/profile",
"style": {
+119
View File
@@ -0,0 +1,119 @@
<template>
<view class="page">
<view class="wrap">
<view class="icon-bg">
<FaIcon name="screwdriver-wrench" color="#2563eb" :size="44" />
</view>
<text class="title">功能开发中</text>
<text v-if="name" class="name">{{ name }}</text>
<text class="desc">该功能正在开发中,暂未开放使用,敬请期待</text>
<view class="btn" @tap="goBack">
<text class="btn-text">返回</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const name = ref('')
onLoad((options) => {
const raw = (options && options.t) || ''
try {
name.value = decodeURIComponent(raw)
} catch (e) {
name.value = raw
}
uni.setNavigationBarTitle({ title: name.value || '功能开发中' })
})
function goBack() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({ url: '/pages/nav/nav' })
}
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
height: 100vh;
background: $color-bg-page;
display: flex;
flex-direction: column;
overflow: hidden;
}
.wrap {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20rpx;
padding: 0 80rpx;
}
.icon-bg {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background: $color-primary-bg;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 16rpx 40rpx rgba(37, 99, 235, 0.16);
}
.title {
margin-top: 16rpx;
font-size: 36rpx;
font-weight: 600;
color: $color-text;
}
.name {
font-size: 28rpx;
font-weight: 500;
color: $color-primary;
word-break: break-all;
text-align: center;
}
.desc {
font-size: 26rpx;
color: $color-text-secondary;
text-align: center;
line-height: 1.6;
}
.btn {
margin-top: 32rpx;
min-width: 240rpx;
height: 84rpx;
padding: 0 40rpx;
border-radius: $radius-full;
background: $color-primary;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10rpx 24rpx rgba(37, 99, 235, 0.28);
&:active {
opacity: 0.9;
}
}
.btn-text {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
}
</style>
+15 -34
View File
@@ -38,7 +38,7 @@
>
<view class="fn-card" :class="{ locked: item.locked }">
<view class="tint-icon fn-icon" :class="item.tint">
<FaIcon :name="item.icon" color="#ffffff" :size="18" />
<FaIcon :name="item.icon" color="#ffffff" :size="20" />
</view>
<text class="fn-name">{{ item.menu.title }}</text>
</view>
@@ -56,12 +56,8 @@
<view v-for="group in groups" :key="group.id" class="group-card">
<view class="group-head">
<view class="tint-icon group-icon" :class="group.tint">
<FaIcon :name="group.icon" color="#ffffff" :size="16" />
</view>
<text class="group-title">{{ group.name }}</text>
<text v-if="group.locked" class="group-lock">未开通</text>
<text class="group-count">{{ group.items.length }}</text>
</view>
<view v-if="group.items.length" class="grid">
@@ -74,9 +70,9 @@
<view class="fn-card" :class="{ locked: group.locked || item.locked }">
<view
class="tint-icon fn-icon"
:class="group.locked || item.locked ? 'tint-off' : group.tint"
:class="group.locked || item.locked ? 'tint-off' : 'tint-' + (item.tintIndex % 8)"
>
<FaIcon :name="item.icon || group.icon" color="#ffffff" :size="18" />
<FaIcon :name="item.icon || group.icon" color="#ffffff" :size="20" />
</view>
<text class="fn-name">{{ item.title }}</text>
</view>
@@ -182,14 +178,15 @@ const groups = computed(() => {
if (common.items.length) all.push(common)
all.push(...dirGroups)
// 组级「未开通」:整组菜单均被锁定;图标底色按分组下标轮换
return all.map((group, gi) => {
// 组级「未开通」:整组菜单均被锁定;卡片浅底色与图标色按展开顺序统一编号轮换
let colorIndex = 0
return all.map((group) => {
const locked = group.items.length > 0 && group.items.every((it) => it.locked === true)
return {
...group,
icon: group.icon || 'file-lines',
tint: locked ? 'tint-off' : 'tint-' + (gi % 8),
locked
locked,
items: group.items.map((it) => ({ ...it, tintIndex: colorIndex++ }))
}
})
})
@@ -213,7 +210,6 @@ const searchResults = computed(() => {
const mod = modules.value.find((x) => x.code === m.module)
return {
menu: m,
moduleName: (mod && mod.name) || '协作工具',
icon: MODULE_ICONS[String(m.module || '').toLowerCase()] || 'file-lines',
tint: mod && mod.enabled === false ? 'tint-off' : 'tint-' + (i % 8),
locked: m.locked === true || (mod && mod.enabled === false)
@@ -326,7 +322,9 @@ onShow(() => {
color: $color-text-muted;
}
/* ============ 分组卡:外层一个 card,内含一行 4 个功能 card ============ */
/* ============ 分组卡:白色卡片 + 一行 4 个功能宫格(图标在上、名称在下) ============
cell 占 25% 并自带右间隙,容器负 margin 抵消末列间隙,
避免使用 calc 除法——小程序端 wxss 对复杂 calc 支持不稳。 */
.group-card {
@include card;
padding: 24rpx 20rpx 8rpx;
@@ -343,16 +341,10 @@ onShow(() => {
padding: 0 4rpx 20rpx;
}
.group-icon {
@include tint-icon(56rpx, 14rpx);
}
.group-title {
@include section-title;
flex: 1;
min-width: 0;
font-size: 28rpx;
font-weight: 600;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -363,13 +355,6 @@ onShow(() => {
flex-shrink: 0;
}
.group-count {
@include chip;
flex-shrink: 0;
}
/* 功能格子:一行 4 个;cell 占 25% 并自带右间隙,容器负 margin 抵消末列间隙,
避免使用 calc 除法——小程序端 wxss 对复杂 calc 支持不稳。 */
.grid {
display: flex;
flex-wrap: wrap;
@@ -379,15 +364,11 @@ onShow(() => {
.cell {
width: 25%;
padding-right: 16rpx;
margin-bottom: 16rpx;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.fn-card {
height: 100%;
padding: 18rpx 6rpx 16rpx;
border-radius: $radius-md;
background: $color-bg-page;
display: flex;
flex-direction: column;
align-items: center;
@@ -400,12 +381,12 @@ onShow(() => {
}
.fn-icon {
@include tint-icon(64rpx, 18rpx);
@include tint-icon(84rpx, 24rpx);
}
.fn-name {
max-width: 100%;
font-size: 22rpx;
font-size: 24rpx;
line-height: 1.3;
color: $color-text;
text-align: center;
+100 -26
View File
@@ -45,20 +45,40 @@
</view>
</block>
<!-- ============ 登录(小程序端微信认证优先,其次账号密码;H5/APK 仅账号密码) ============ -->
<!-- ============ 登录(小程序端微信/账号密码 Tab 切换;H5/APK 仅账号密码) ============ -->
<block v-else>
<view class="panel-head">
<text class="panel-title">欢迎登录</text>
<text class="panel-sub">{{ showMiniLogin ? '使用微信一键登录,或使用账号密码继续' : '请输入您的账号与密码以继续' }}</text>
<text class="panel-sub">{{ panelSub }}</text>
</view>
<view v-if="errorMsg" class="error-box">
<text class="error-text">{{ errorMsg }}</text>
</view>
<!-- 微信小程序:微信认证登录为主入口 -->
<!-- #ifdef MP-WEIXIN -->
<block v-if="showMiniLogin">
<!-- 微信端且服务端已开通:微信登录 / 账号密码登录 Tab 切换 -->
<view v-if="showMiniLogin" class="login-tabs">
<view
class="login-tab"
:class="{ on: activeTab === 'wechat' }"
@tap="switchTab('wechat')"
>
<text class="login-tab-text">微信登录</text>
<view class="login-tab-bar" />
</view>
<view
class="login-tab"
:class="{ on: activeTab === 'account' }"
@tap="switchTab('account')"
>
<text class="login-tab-text">账号密码登录</text>
<view class="login-tab-bar" />
</view>
</view>
<!-- 微信登录面板 -->
<block v-if="showMiniLogin && activeTab === 'wechat'">
<button
class="btn-wechat"
open-type="getPhoneNumber"
@@ -71,14 +91,14 @@
<view class="tip-row">
<text class="tip-text">首次使用请先用账号密码登录一次,自动完成微信绑定后即可一键登录</text>
</view>
<view class="divider">
<view class="divider-line" />
<text class="divider-text">使用账号密码登录</text>
<view class="divider-line" />
<view class="back-row" @tap="switchTab('account')">
<text class="link">使用账号密码登录</text>
</view>
</block>
<!-- #endif -->
<!-- 账号密码面板(未开通微信登录的端始终展示) -->
<block v-if="!(showMiniLogin && activeTab === 'wechat')">
<view class="field">
<text class="field-label">账号</text>
<view class="input-wrap">
@@ -128,15 +148,18 @@
>
<text>{{ submitting ? '登录中...' : '登 录' }}</text>
</view>
<view v-if="!agreed && showAgreementTip" class="agree-tip">
<FaIcon name="circle-info" color="#f79009" :size="12" />
<text class="agree-tip-text">请先阅读并勾选同意《用户协议》和《隐私政策》</text>
</view>
<view class="back-row">
<text class="tip-text">还没有账号?</text>
<text class="link" @tap="goRegister">立即注册</text>
</view>
</block>
<!-- 未勾选协议提示:两个 Tab 面板共用 -->
<view v-if="!agreed && showAgreementTip" class="agree-tip">
<FaIcon name="circle-info" color="#f79009" :size="12" />
<text class="agree-tip-text">请先阅读并勾选同意《用户协议》和《隐私政策》</text>
</view>
</block>
</view>
@@ -158,7 +181,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import {
loginUAC,
switchTenant,
@@ -186,6 +209,8 @@ import { showGeetest4 } from '@/utils/geetest.js'
import FaIcon from '@/components/FaIcon.vue'
const step = ref('password')
/** 小程序端登录方式 Tab:wechat / account(其他端固定账号密码) */
const activeTab = ref('wechat')
const account = ref('')
const password = ref('')
const showPassword = ref(false)
@@ -201,6 +226,22 @@ const verifyCfg = ref({ needGeetest: false, captchaId: '' })
/** 仅微信小程序端且服务端已开通时展示「一键登录」入口 */
const showMiniLogin = ref(false)
/** 面板副标题:随 Tab 切换提示文案 */
const panelSub = computed(() => {
if (platform.value === 'mp-weixin' && showMiniLogin.value && activeTab.value === 'wechat') {
return '使用微信一键登录,或切换到账号密码登录'
}
return '请输入您的账号与密码以继续'
})
/** 切换登录方式 Tab:清掉上一次残留的错误提示 */
function switchTab(tab) {
if (submitting.value) return
if (activeTab.value === tab) return
activeTab.value = tab
errorMsg.value = ''
}
/** 运行端检测:h5 / apk(app-plus) / mp-weixin,决定登录方式优先级 */
const platform = ref('')
// #ifdef H5
@@ -223,6 +264,8 @@ onMounted(async () => {
if (remembered.rememberMe) {
rememberMe.value = true
account.value = remembered.account
// 记住了账号,默认落在账号密码 Tab 更顺手
activeTab.value = 'account'
}
loadVerifyConfig()
@@ -479,10 +522,15 @@ function handleMiniError(err) {
if (reason === 'identity_not_found') {
uni.showModal({
title: '尚未绑定云泽账号',
content: '当前微信未绑定云泽账号,且手机号能力暂不可用。请先在下方用账号密码登录一次(会自动完成微信绑定),之后即可一键登录。',
showCancel: false,
content: '当前微信未绑定云泽账号,且手机号能力暂不可用。请先用账号密码登录一次(会自动完成微信绑定),之后即可一键登录。',
showCancel: true,
cancelText: '知道了',
// 微信小程序 showModal 的 confirmText 上限为 4 个汉字,超限会导致 API 失败、弹窗不显示
confirmText: '密码登录'
confirmText: '去登录',
confirmColor: '#2563eb',
success: (res) => {
if (res.confirm) switchTab('account')
}
})
return
}
@@ -791,22 +839,48 @@ function goForget() {
}
}
.divider {
/* 登录方式 Tab:微信登录 / 账号密码登录 */
.login-tabs {
display: flex;
align-items: center;
gap: 16rpx;
margin: 40rpx 0 24rpx;
gap: 48rpx;
margin-bottom: 36rpx;
border-bottom: 1rpx solid $color-border;
}
.divider-line {
flex: 1;
height: 1rpx;
background: $color-border;
.login-tab {
position: relative;
padding: 0 4rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.divider-text {
font-size: 24rpx;
color: #a3acbf;
.login-tab-text {
font-size: 28rpx;
color: $color-text-secondary;
}
.login-tab.on .login-tab-text {
font-size: 30rpx;
font-weight: 600;
color: #2563eb;
}
.login-tab-bar {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 6rpx;
border-radius: 6rpx;
background: #2563eb;
transition: width 0.2s ease;
}
.login-tab.on .login-tab-bar {
width: 56rpx;
}
.tip-row {
+29 -10
View File
@@ -7,8 +7,8 @@
<view class="hero-info">
<view class="hero-title-row">
<text class="hero-title">本周日程</text>
<text class="hero-unit">(项)</text>
<text class="hero-count">{{ weekCount }}</text>
<!-- <text class="hero-unit">(项)</text> -->
<!-- <text class="hero-count">{{ weekCount }}</text> -->
</view>
<text class="hero-sub">{{ heroRangeLabel }}</text>
</view>
@@ -55,10 +55,10 @@
>{{ cell.day }}</view>
<view class="cal-dots">
<view
v-for="n in dotCount(cell.key)"
:key="n"
v-for="(dot, di) in dayDots(cell.key)"
:key="di"
class="cal-dot"
:class="{ on: cell.key === selectedKey }"
:class="[dot, { on: cell.key === selectedKey }]"
/>
</view>
</view>
@@ -246,8 +246,12 @@ const selectedLabel = computed(() => {
return `${d.getMonth() + 1}月${d.getDate()}日 ${week}`
})
function dotCount(key) {
return Math.min(3, (dayMap.value[key] || []).length)
/** 日历标记点:未完成(蓝)靠左优先排,已完成(绿)靠后,最多 3 个 */
function dayDots(key) {
const list = dayMap.value[key] || []
const pending = list.filter((item) => !item.completed)
const done = list.filter((item) => item.completed)
return pending.concat(done).slice(0, 3).map((item) => (item.completed ? 'done' : 'pending'))
}
/** 时间展示:全天显示「全天」,其余显示开始时间或起止区间 */
@@ -525,10 +529,25 @@ function openDetail(id) {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: $color-primary;
background: $color-danger;
&.on {
background: #ffffff;
/* 未完成:红色 */
&.pending {
background: $color-danger;
}
/* 已完成:绿色 */
&.done {
background: $color-success;
}
/* 选中日:红点转浅红、绿点转浅绿,保证在深蓝底上的对比度 */
&.pending.on {
background: #fca5a5;
}
&.done.on {
background: #6ee7b7;
}
}
+36 -7
View File
@@ -1,9 +1,12 @@
/**
* 菜单 → 页面 的跳转分发
*
* 聚合平台里绝大多数业务功能(ERP / OA / CRM / CMS)目前只有 Web 端实现,
* 端上没有对应页面;少数功能(记事本、日程提醒)已有原生页面。
* 这里统一判定:能走原生就走原生,否则丢进 webview 容器打开 Web 端深链。
* 小程序端「已开发」的只有少数原生页面(记事本、日程、OA 若干),
* 其余菜单在端上都没有对应页面:过去统一丢进 webview 打开 Web 端,
* 而 Web 端没登录态会被路由守卫送到统一认证登录页,用户看到的是「登录」而不是功能。
*
* 现在的判定:命中原生页 → 打开原生页;命中 WEB_RULES(Web 端确已上线、可免登录打开)
* → 走 webview;其余一律进入「功能开发中」占位页。
*/
import { createAppTicket } from '@/api/auth.js'
@@ -26,6 +29,23 @@ const NATIVE_RULES = [
/** Web 端容器页 */
const WEBVIEW_PAGE = '/pages/apps/webview'
/**
* 确已在 Web 端上线、可以免登录直接打开的功能(按菜单 title 或 path 关键词匹配)。
* 命中 → webview 打开 Web 端;未命中 → 「功能开发中」占位页。
* 新功能上线时把对应关键词加进来即可。
*/
const WEB_RULES = []
/** 开发中原生占位页 */
const DEVELOPING_PAGE = '/pages/common/developing'
/** 该功能在小程序端尚未开发 */
function openDeveloping(name) {
uni.navigateTo({
url: DEVELOPING_PAGE + '?t=' + encodeURIComponent(name || '功能')
})
}
/** 菜单是否命中移动端原生页面 */
export function resolveNativeRoute(menu = {}) {
const haystack = (menu.title || '') + ' ' + (menu.path || '')
@@ -73,13 +93,22 @@ export function openMenu(menu, fallbackTitle = '') {
return
}
if (!menu.path) {
uni.showToast({ title: name + '暂未开放', icon: 'none' })
const target = String(menu.path || '').trim()
// 完整外链直接打开,不走开发中判定
if (/^https?:\/\//i.test(target)) {
openRawUrl(target, name)
return
}
// 走 Web 端:先换一次性票据实现免登录,失败则降级为直接打开(用户手动登录一次)
openWebPage(menu.path, name)
// Web 端确已上线的功能仍走 webview(带一次性票据免登录)
const haystack = name + ' ' + target
if (target && WEB_RULES.some((kw) => haystack.indexOf(kw) !== -1)) {
openWebPage(target, name)
return
}
// 端上没有对应页面 = 功能未开发:进占位页,不再跳 Web 的统一认证登录
openDeveloping(name)
}
/**