更新
This commit is contained in:
1 parent
48f3bd997c
commit
184d5b0403
9 files changed
+513
-115
No files matched your search
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -289,6 +289,14 @@
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/common/developing",
|
||||
"style": {
|
||||
"navigationBarTitleText": "功能开发中",
|
||||
"navigationBarBackgroundColor": "#F8FAFC",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user