优化uniapp
This commit is contained in:
1 parent
4f1acaa451
commit
8fc52c574f
49 files changed
+1926
-171
No files matched your search
@@ -1 +0,0 @@
|
|||||||
{"tenant_name":"连云港云泽广告传媒有限公司","account":"hero920103","password":"920103","lot_number":"test","pass_token":"test","gen_time":"test","captcha_output":"test"}
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<span class="logo-grid"><i></i><i></i><i></i><i></i></span>
|
<span class="logo-grid"><i></i><i></i><i></i><i></i></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="brand-meta">
|
<span class="brand-meta">
|
||||||
<span class="brand-title">云泽管理平台</span>
|
<span class="brand-title">温新云平台</span>
|
||||||
<span class="brand-sub">{{ currentModule?.title || "数据赋能管理 · 科技创造价值" }}</span>
|
<span class="brand-sub">{{ currentModule?.title || "数据赋能管理 · 科技创造价值" }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1378,7 +1378,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-cell:hover {
|
.calendar-cell:hover {
|
||||||
background: #f5f8ff;
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#fafcff 0,
|
||||||
|
#fafcff 6px,
|
||||||
|
#edf4ff 6px,
|
||||||
|
#edf4ff 12px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-cell.other-month {
|
.calendar-cell.other-month {
|
||||||
@@ -1386,6 +1392,16 @@ onMounted(() => {
|
|||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-cell.other-month:hover {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#fafbfc 0,
|
||||||
|
#fafbfc 6px,
|
||||||
|
#f2f4f8 6px,
|
||||||
|
#f2f4f8 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.cell-date {
|
.cell-date {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1435,7 +1451,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-cell.is-off:hover {
|
.calendar-cell.is-off:hover {
|
||||||
background: #fff5f5;
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#fff7f7 0,
|
||||||
|
#fff7f7 7px,
|
||||||
|
#fde8e8 7px,
|
||||||
|
#fde8e8 14px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 调休上班日的周六日按工作日处理,弱化底色避免误判为休息 */
|
/* 调休上班日的周六日按工作日处理,弱化底色避免误判为休息 */
|
||||||
@@ -1443,6 +1465,16 @@ onMounted(() => {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-cell.is-work:hover {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#ffffff 0,
|
||||||
|
#ffffff 6px,
|
||||||
|
#f2f5f9 6px,
|
||||||
|
#f2f5f9 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* 我的排班背景:上班绿 / 休息橙。排班是实际作息,覆盖节假日底色,
|
/* 我的排班背景:上班绿 / 休息橙。排班是实际作息,覆盖节假日底色,
|
||||||
但保留休/班角标与节日名;today / selected 在后面定义,优先级仍最高。
|
但保留休/班角标与节日名;today / selected 在后面定义,优先级仍最高。
|
||||||
仅靠提高底色饱和度区分,不加边框/竖条 */
|
仅靠提高底色饱和度区分,不加边框/竖条 */
|
||||||
@@ -1451,7 +1483,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-cell.is-my-work:hover {
|
.calendar-cell.is-my-work:hover {
|
||||||
background: #d3f0bd;
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#e2f6d2 0,
|
||||||
|
#e2f6d2 7px,
|
||||||
|
#d2efbe 7px,
|
||||||
|
#d2efbe 14px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 休息日:斜纹底 + 右上角「休」角标。
|
/* 休息日:斜纹底 + 右上角「休」角标。
|
||||||
@@ -1514,6 +1552,16 @@ onMounted(() => {
|
|||||||
);
|
);
|
||||||
box-shadow: inset 0 0 0 2px #409eff;
|
box-shadow: inset 0 0 0 2px #409eff;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#e0efff 0,
|
||||||
|
#e0efff 6px,
|
||||||
|
#cbe4ff 6px,
|
||||||
|
#cbe4ff 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 今天:不再用实底蓝,改为绿色描边框,保留排班底色(上班绿 / 休息斜纹)不变。
|
/* 今天:不再用实底蓝,改为绿色描边框,保留排班底色(上班绿 / 休息斜纹)不变。
|
||||||
@@ -1830,21 +1878,27 @@ html.dark & {
|
|||||||
border-bottom-color: var(--el-border-color-lighter);
|
border-bottom-color: var(--el-border-color-lighter);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--el-fill-color-light);
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.03) 0,
|
||||||
|
rgba(255, 255, 255, 0.03) 6px,
|
||||||
|
rgba(255, 255, 255, 0.08) 6px,
|
||||||
|
rgba(255, 255, 255, 0.08) 12px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
&.other-month {
|
&.other-month {
|
||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-text-color-placeholder);
|
||||||
background: var(--el-fill-color-light);
|
background: var(--el-fill-color-light);
|
||||||
}
|
|
||||||
&.selected {
|
&:hover {
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(64, 158, 255, 0.14) 0,
|
rgba(255, 255, 255, 0.02) 0,
|
||||||
rgba(64, 158, 255, 0.14) 6px,
|
rgba(255, 255, 255, 0.02) 6px,
|
||||||
rgba(64, 158, 255, 0.3) 6px,
|
rgba(255, 255, 255, 0.06) 6px,
|
||||||
rgba(64, 158, 255, 0.3) 12px
|
rgba(255, 255, 255, 0.06) 12px
|
||||||
);
|
);
|
||||||
box-shadow: inset 0 0 0 2px var(--el-color-primary);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.month-tag {
|
.month-tag {
|
||||||
@@ -1865,24 +1919,80 @@ html.dark & {
|
|||||||
background: rgba(245, 108, 108, 0.07);
|
background: rgba(245, 108, 108, 0.07);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(245, 108, 108, 0.12);
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(245, 108, 108, 0.06) 0,
|
||||||
|
rgba(245, 108, 108, 0.06) 7px,
|
||||||
|
rgba(245, 108, 108, 0.16) 7px,
|
||||||
|
rgba(245, 108, 108, 0.16) 14px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.calendar-cell.is-work {
|
.calendar-cell.is-work {
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.03) 0,
|
||||||
|
rgba(255, 255, 255, 0.03) 6px,
|
||||||
|
rgba(255, 255, 255, 0.08) 6px,
|
||||||
|
rgba(255, 255, 255, 0.08) 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.calendar-cell.is-my-work {
|
.calendar-cell.is-my-work {
|
||||||
background: rgba(103, 194, 58, 0.1);
|
background: rgba(103, 194, 58, 0.1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(103, 194, 58, 0.16);
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(103, 194, 58, 0.08) 0,
|
||||||
|
rgba(103, 194, 58, 0.08) 7px,
|
||||||
|
rgba(103, 194, 58, 0.2) 7px,
|
||||||
|
rgba(103, 194, 58, 0.2) 14px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 休息日:保持浅色主题的斜纹语言,仅用半透明琥珀色适配深色底,斜纹在暗底仍可辨 */
|
||||||
.calendar-cell.is-my-rest {
|
.calendar-cell.is-my-rest {
|
||||||
background: rgba(230, 162, 60, 0.1);
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(230, 162, 60, 0.07) 0,
|
||||||
|
rgba(230, 162, 60, 0.07) 7px,
|
||||||
|
rgba(230, 162, 60, 0.16) 7px,
|
||||||
|
rgba(230, 162, 60, 0.16) 14px
|
||||||
|
);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(230, 162, 60, 0.16);
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(230, 162, 60, 0.12) 0,
|
||||||
|
rgba(230, 162, 60, 0.12) 7px,
|
||||||
|
rgba(230, 162, 60, 0.24) 7px,
|
||||||
|
rgba(230, 162, 60, 0.24) 14px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 选中态:暗色下优先级高于所有排班与节假日底色,保持蓝色斜条纹与主色描边 */
|
||||||
|
.calendar-cell.selected {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(64, 158, 255, 0.15) 0,
|
||||||
|
rgba(64, 158, 255, 0.15) 6px,
|
||||||
|
rgba(64, 158, 255, 0.32) 6px,
|
||||||
|
rgba(64, 158, 255, 0.32) 12px
|
||||||
|
);
|
||||||
|
box-shadow: inset 0 0 0 2px var(--el-color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(64, 158, 255, 0.24) 0,
|
||||||
|
rgba(64, 158, 255, 0.24) 6px,
|
||||||
|
rgba(64, 158, 255, 0.44) 6px,
|
||||||
|
rgba(64, 158, 255, 0.44) 12px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* 今天:与浅色主题一致,只用绿色描边框,不覆盖排班底色 */
|
/* 今天:与浅色主题一致,只用绿色描边框,不覆盖排班底色 */
|
||||||
|
|||||||
@@ -46,6 +46,15 @@
|
|||||||
<div class="sub-text">{{ scope.row.identity_mobile || "" }}</div>
|
<div class="sub-text">{{ scope.row.identity_mobile || "" }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="挂靠资料" min-width="170" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<div>{{ scope.row.applicant_name || "-" }}</div>
|
||||||
|
<div class="sub-text">
|
||||||
|
{{ scope.row.department_name || "-" }} · {{ scope.row.position_name || "-" }}
|
||||||
|
</div>
|
||||||
|
<div class="sub-text">微信手机号 {{ scope.row.wechat_mobile || "-" }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="联系方式" min-width="150" align="center">
|
<el-table-column label="联系方式" min-width="150" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div>{{ scope.row.contact_person || "-" }}</div>
|
<div>{{ scope.row.contact_person || "-" }}</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<div class="home-brand">
|
<div class="home-brand">
|
||||||
<span class="home-logo"><i></i><i></i><i></i><i></i></span>
|
<span class="home-logo"><i></i><i></i><i></i><i></i></span>
|
||||||
<span class="home-brand-name">云泽管理平台</span>
|
<span class="home-brand-name">温新云平台</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 当前企业 + 一人多企业时可点击切换 -->
|
<!-- 当前企业 + 一人多企业时可点击切换 -->
|
||||||
<TenantSwitcher variant="plain" />
|
<TenantSwitcher variant="plain" />
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="home-footer">
|
<div class="home-footer">
|
||||||
<span class="copyright">云泽管理平台</span>
|
<span class="copyright">温新云平台</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- 系统基本信息 -->
|
<!-- 系统基本信息 -->
|
||||||
<el-descriptions title="系统基本信息" :column="2" border class="info-section">
|
<el-descriptions title="系统基本信息" :column="2" border class="info-section">
|
||||||
<el-descriptions-item label="系统名称">
|
<el-descriptions-item label="系统名称">
|
||||||
<el-tag type="primary">{{ systemInfo.name || '云泽管理系统' }}</el-tag>
|
<el-tag type="primary">{{ systemInfo.name || '温新云平台' }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="系统版本">
|
<el-descriptions-item label="系统版本">
|
||||||
<el-tag>{{ systemInfo.version || '1.0.0' }}</el-tag>
|
<el-tag>{{ systemInfo.version || '1.0.0' }}</el-tag>
|
||||||
@@ -227,7 +227,7 @@ async function fetchSystemInfo() {
|
|||||||
|
|
||||||
// 模拟数据(如果有API会替换)
|
// 模拟数据(如果有API会替换)
|
||||||
systemInfo.value = {
|
systemInfo.value = {
|
||||||
name: '云泽管理系统',
|
name: '温新云平台',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
environment: 'Production',
|
environment: 'Production',
|
||||||
uptime: '24天 5小时 30分钟',
|
uptime: '24天 5小时 30分钟',
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ func (c *AppAggregateController) Home() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tid := uint64(claims.TenantId)
|
tid := uint64(claims.TenantId)
|
||||||
|
// 注册即登录:新用户尚未加入企业时令牌 tid=0。
|
||||||
|
// 此时按「原免登录功能」返回公开模块与公开菜单,其余模块由前端引导创建/挂靠企业。
|
||||||
|
if tid == 0 {
|
||||||
|
c.serveJSON(map[string]interface{}{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": c.noTenantHome(claims),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
uid := services.ResolveTenantUID(tid, uint64(claims.UserID))
|
uid := services.ResolveTenantUID(tid, uint64(claims.UserID))
|
||||||
|
|
||||||
modules := c.appModules(tid)
|
modules := c.appModules(tid)
|
||||||
@@ -125,6 +135,77 @@ func (c *AppAggregateController) Home() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noTenantHome tid=0(已注册、尚未加入企业)时的首屏数据。
|
||||||
|
// 模块沿用公开宫格(保留 require_login 作为「需企业」标记),菜单只下发免登录菜单,
|
||||||
|
// 因此新注册用户能直接使用原「免登录」功能,进入需企业模块时由端上引导申请。
|
||||||
|
func (c *AppAggregateController) noTenantHome(claims *jwtutil.Claims) map[string]interface{} {
|
||||||
|
var modRows []models.SystemModules
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.SystemModules)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
Filter("status", 1).
|
||||||
|
Filter("is_show", 1).
|
||||||
|
OrderBy("sort", "id").
|
||||||
|
All(&modRows)
|
||||||
|
modRows = services.FilterModulesByPlatform(modRows, models.PlatformApp)
|
||||||
|
modules := make([]publicModuleItem, 0, len(modRows))
|
||||||
|
for _, m := range modRows {
|
||||||
|
modules = append(modules, publicModuleItem{
|
||||||
|
ID: m.ID, Code: m.Code, Name: m.Name, Icon: m.Icon,
|
||||||
|
Path: m.Path, Description: m.Description, RequireLogin: m.RequireLogin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var menuRows []models.SystemMenu
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.SystemMenu)).
|
||||||
|
Filter("status", 1).
|
||||||
|
Filter("require_login", 0).
|
||||||
|
All(&menuRows)
|
||||||
|
allModules := c.allModules()
|
||||||
|
menus := make([]appMenuItem, 0, len(menuRows))
|
||||||
|
for _, m := range menuRows {
|
||||||
|
if m.Type == 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := appMenuItem{
|
||||||
|
ID: m.ID, Pid: m.Pid, Type: m.Type, Sort: m.Sort,
|
||||||
|
Title: strings.TrimSpace(m.Title), Leaf: true,
|
||||||
|
}
|
||||||
|
if m.Path != nil {
|
||||||
|
item.Path = strings.TrimSpace(*m.Path)
|
||||||
|
}
|
||||||
|
if m.Icon != nil {
|
||||||
|
item.Icon = strings.TrimSpace(*m.Icon)
|
||||||
|
}
|
||||||
|
item.Module = appModuleCodeOfPath(item.Path, allModules)
|
||||||
|
menus = append(menus, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := "none"
|
||||||
|
var apply models.AuthTenantApply
|
||||||
|
if err := models.Orm.QueryTable(new(models.AuthTenantApply)).
|
||||||
|
Filter("identity_id", uint64(claims.UserID)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
OrderBy("-id").One(&apply); err == nil {
|
||||||
|
switch apply.Status {
|
||||||
|
case models.TenantApplyStatusPending:
|
||||||
|
state = "pending"
|
||||||
|
case models.TenantApplyStatusReject:
|
||||||
|
state = "rejected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"user": map[string]interface{}{
|
||||||
|
"account": claims.Username, "tenant": "", "tid": 0, "need_tenant": true,
|
||||||
|
},
|
||||||
|
"modules": modules,
|
||||||
|
"menus": menus,
|
||||||
|
"stats": map[string]interface{}{},
|
||||||
|
"quick_menu_ids": []uint64{},
|
||||||
|
"tenant_state": state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// publicModuleItem 移动端免登录可见的模块卡片:只暴露展示与安全开关,不含菜单/KPI。
|
// publicModuleItem 移动端免登录可见的模块卡片:只暴露展示与安全开关,不含菜单/KPI。
|
||||||
// 供未登录用户渲染 nav 宫格,require_login 决定点击是「进登录引导」还是「直接进入」。
|
// 供未登录用户渲染 nav 宫格,require_login 决定点击是「进登录引导」还是「直接进入」。
|
||||||
type publicModuleItem struct {
|
type publicModuleItem struct {
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func (c *AuthAppTicketController) Exchange() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username := "云泽用户"
|
username := "温新云用户"
|
||||||
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
|
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
|
||||||
username = strings.TrimSpace(*identity.Nickname)
|
username = strings.TrimSpace(*identity.Nickname)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// 小程序登录失败原因(前端按此分支给出不同引导)
|
// 小程序登录失败原因(前端按此分支给出不同引导)
|
||||||
const (
|
const (
|
||||||
MiniReasonNotConfigured = "not_configured" // 服务端未配置小程序
|
MiniReasonNotConfigured = "not_configured" // 服务端未配置小程序
|
||||||
MiniReasonIdentityNotFound = "identity_not_found" // 微信未匹配到云泽账号
|
MiniReasonIdentityNotFound = "identity_not_found" // 微信未匹配到温新云账号
|
||||||
MiniReasonNoTenant = "no_tenant" // 账号未加入任何企业
|
MiniReasonNoTenant = "no_tenant" // 账号未加入任何企业
|
||||||
MiniReasonWechatAPI = "wechat_api_error" // 微信接口报错
|
MiniReasonWechatAPI = "wechat_api_error" // 微信接口报错
|
||||||
)
|
)
|
||||||
@@ -120,7 +120,7 @@ func (c *AuthWechatMiniController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定位云泽身份:手机号优先(已通过微信验证),其次查已绑定的小程序 openid
|
// 定位温新云身份:手机号优先(已通过微信验证),其次查已绑定的小程序 openid
|
||||||
var mobile string
|
var mobile string
|
||||||
if req.PhoneCode != "" {
|
if req.PhoneCode != "" {
|
||||||
phone, perr := wechatmini.GetPhoneNumber(cfg, req.PhoneCode)
|
phone, perr := wechatmini.GetPhoneNumber(cfg, req.PhoneCode)
|
||||||
@@ -137,7 +137,7 @@ func (c *AuthWechatMiniController) Login() {
|
|||||||
|
|
||||||
identity, err := c.resolveIdentity(cfg, mobile, session)
|
identity, err := c.resolveIdentity(cfg, mobile, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.fail(MiniReasonIdentityNotFound, 404, "该微信未绑定云泽账号,请使用手机号一键登录或先注册")
|
c.fail(MiniReasonIdentityNotFound, 404, "该微信未绑定温新云账号,请使用手机号一键登录或先注册")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if identity.Status != models.AuthIdentityStatusEnabled {
|
if identity.Status != models.AuthIdentityStatusEnabled {
|
||||||
@@ -147,15 +147,18 @@ func (c *AuthWechatMiniController) Login() {
|
|||||||
|
|
||||||
// 企业选择:单企业直接进入,多企业签发待选令牌由前端展示选择界面
|
// 企业选择:单企业直接进入,多企业签发待选令牌由前端展示选择界面
|
||||||
tenants, lerr := authsvc.ListTenantOptions(identity.ID)
|
tenants, lerr := authsvc.ListTenantOptions(identity.ID)
|
||||||
if lerr != nil || len(tenants) == 0 {
|
if lerr != nil {
|
||||||
c.fail(MiniReasonNoTenant, 403, "该账号未加入任何企业,请联系管理员")
|
c.fail("", 400, "获取企业列表失败,请重试")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 新流程:账号已注册但尚未加入企业时仍签发正式令牌(tid=0),
|
||||||
|
// 端上可继续使用原免登录模块,并在进入需企业模块时引导创建/挂靠企业。
|
||||||
|
noTenant := len(tenants) == 0
|
||||||
tid := uint64(0)
|
tid := uint64(0)
|
||||||
needChoose := false
|
needChoose := false
|
||||||
if len(tenants) == 1 {
|
if len(tenants) == 1 {
|
||||||
tid = tenants[0].Tid
|
tid = tenants[0].Tid
|
||||||
} else {
|
} else if len(tenants) > 1 {
|
||||||
tid = authsvc.PendingTenantID
|
tid = authsvc.PendingTenantID
|
||||||
needChoose = true
|
needChoose = true
|
||||||
}
|
}
|
||||||
@@ -179,6 +182,7 @@ func (c *AuthWechatMiniController) Login() {
|
|||||||
"tokens": tokens,
|
"tokens": tokens,
|
||||||
"tenants": tenants,
|
"tenants": tenants,
|
||||||
"identity": map[string]interface{}{"id": identity.ID, "nickname": username, "mobile": mobileOr(identity)},
|
"identity": map[string]interface{}{"id": identity.ID, "nickname": username, "mobile": mobileOr(identity)},
|
||||||
|
"need_tenant": noTenant,
|
||||||
}
|
}
|
||||||
if needChoose {
|
if needChoose {
|
||||||
resp["need_choose_tenant"] = true
|
resp["need_choose_tenant"] = true
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func (c *GuestController) MiniLogin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手机号已注册云泽账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。
|
// 手机号已注册温新云账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。
|
||||||
// (与 /auth/wechat/miniapp/login 同一套识别与签发逻辑)
|
// (与 /auth/wechat/miniapp/login 同一套识别与签发逻辑)
|
||||||
if c.tryUserLogin(mobile, session) {
|
if c.tryUserLogin(mobile, session) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -206,9 +206,9 @@ func (c *IndexPortalController) GetFooterData() {
|
|||||||
|
|
||||||
// 准备默认的 Footer 配置数据
|
// 准备默认的 Footer 配置数据
|
||||||
footerMap := map[string]string{
|
footerMap := map[string]string{
|
||||||
"sitename": "云泽官方网站",
|
"sitename": "温新云官方网站",
|
||||||
"companyname": "云泽智能科技有限公司",
|
"companyname": "连云港温新智能科技有限公司",
|
||||||
"copyright": "Copyright © 2026 Yunzer. All Rights Reserved.",
|
"copyright": "Copyright © 2026 WENXIN. All Rights Reserved.",
|
||||||
"icp": "苏ICP备2026000000号-1",
|
"icp": "苏ICP备2026000000号-1",
|
||||||
"description": "专业为企业建设数字化管理与官网门户的一站式 SaaS 服务平台",
|
"description": "专业为企业建设数字化管理与官网门户的一站式 SaaS 服务平台",
|
||||||
"logo": "",
|
"logo": "",
|
||||||
|
|||||||
@@ -0,0 +1,471 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
// ==================== 小程序新用户引导 ====================
|
||||||
|
//
|
||||||
|
// 新的工作台口径:不再区分「登录 / 免登录」。
|
||||||
|
// 1. 点任意模块卡片 → 微信手机号授权;
|
||||||
|
// 2. 手机号已存在 → 前端走 /auth/wechat/miniapp/login 静默登录(自动绑定 openid);
|
||||||
|
// 不存在 → 本文件 Phone 签发注册票据,Register 收 ≥6 位密码后建号并绑定微信,
|
||||||
|
// 同时签发 tid=0 的正式令牌(注册即登录,可直接使用原免登录模块);
|
||||||
|
// 3. 进入需企业模块前,Status 查企业绑定 / 申请状态;未通过则走创建企业或挂靠企业;
|
||||||
|
// 4. 两类申请复用 services 层已有的审核闭环(platform / backend 审批端无需改动)。
|
||||||
|
//
|
||||||
|
// 本组路由均为公开路径(不在 middleware.protectedPrefixes 内):
|
||||||
|
// phone / register 由微信凭证与一次性注册票据自证,其余端点自行校验 Bearer 令牌。
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
"server/pkg/identity"
|
||||||
|
"server/pkg/jwtutil"
|
||||||
|
"server/pkg/passwordutil"
|
||||||
|
"server/services"
|
||||||
|
authsvc "server/services/auth"
|
||||||
|
"server/services/wechatmini"
|
||||||
|
|
||||||
|
"github.com/beego/beego/v2/core/logs"
|
||||||
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
const onboardingClientID = "yz-miniapp"
|
||||||
|
|
||||||
|
type OnboardingController struct {
|
||||||
|
beego.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) reply(status int, msg string, data interface{}) {
|
||||||
|
c.Ctx.Output.SetStatus(status)
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": status, "msg": msg, "data": data}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) ok(data interface{}) {
|
||||||
|
c.reply(200, "success", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) fail(status int, msg string) {
|
||||||
|
c.reply(status, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) readJSON(v interface{}) bool {
|
||||||
|
body := c.Ctx.Input.RequestBody
|
||||||
|
if len(body) == 0 {
|
||||||
|
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, v); err != nil {
|
||||||
|
c.fail(400, "参数错误")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// claims 当前登录态;允许 tid=0(刚注册、尚未加入企业)的正式令牌。
|
||||||
|
func (c *OnboardingController) claims() (*jwtutil.Claims, error) {
|
||||||
|
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) mustClaims() *jwtutil.Claims {
|
||||||
|
cl, err := c.claims()
|
||||||
|
if err != nil || cl == nil || cl.UserID <= 0 {
|
||||||
|
c.fail(401, "登录态已失效,请重新授权手机号")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone POST /api/onboarding/phone
|
||||||
|
// body: { code(wx.login), phone_code(getPhoneNumber), appid? }
|
||||||
|
// 返回 exists / need_register;无账号时附带一次性 register_token(内含微信手机号与 openid)。
|
||||||
|
func (c *OnboardingController) Phone() {
|
||||||
|
var req struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
PhoneCode string `json:"phone_code"`
|
||||||
|
AppID string `json:"appid"`
|
||||||
|
}
|
||||||
|
if !c.readJSON(&req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg, err := loadGuestMiniConfig(req.AppID)
|
||||||
|
if err != nil {
|
||||||
|
c.fail(400, "小程序未配置,无法获取手机号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
phone, perr := wechatmini.GetPhoneNumber(cfg, strings.TrimSpace(req.PhoneCode))
|
||||||
|
if perr != nil {
|
||||||
|
msg := perr.Error()
|
||||||
|
if hint, ok := wechatmini.ExplainError(perr); ok {
|
||||||
|
msg = hint
|
||||||
|
}
|
||||||
|
logs.Warn("[onboarding] 获取手机号失败: %v", perr)
|
||||||
|
c.fail(400, "手机号授权失败:"+msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mobile := strings.TrimSpace(phone.PurePhoneNumber)
|
||||||
|
if mobile == "" {
|
||||||
|
mobile = strings.TrimSpace(phone.PhoneNumber)
|
||||||
|
}
|
||||||
|
if mobile == "" {
|
||||||
|
c.fail(400, "未获取到手机号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已注册:交给前端用现有小程序一键登录静默登录(自动补绑 openid)
|
||||||
|
if exist := identityByPhone(mobile); exist != nil {
|
||||||
|
c.ok(map[string]interface{}{
|
||||||
|
"exists": true,
|
||||||
|
"need_register": false,
|
||||||
|
"mobile_mask": maskPhone(mobile),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册必须同时绑定微信号:拿不到 openid 时不签发注册票据,避免留下「无绑定」账号
|
||||||
|
session := guestMiniSession(cfg, req.Code)
|
||||||
|
if session == nil || strings.TrimSpace(session.OpenID) == "" {
|
||||||
|
c.fail(400, "微信身份获取失败,请重新进入小程序授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := services.IssuePendingRegisterToken(services.RegisterPending{
|
||||||
|
Mobile: mobile,
|
||||||
|
OpenID: session.OpenID,
|
||||||
|
UnionID: session.UnionID,
|
||||||
|
Provider: models.IdPWechatMini,
|
||||||
|
})
|
||||||
|
if token == "" {
|
||||||
|
c.fail(500, "注册会话创建失败,请稍后重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{
|
||||||
|
"exists": false,
|
||||||
|
"need_register": true,
|
||||||
|
"register_token": token,
|
||||||
|
"mobile_mask": maskPhone(mobile),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register POST /api/onboarding/register
|
||||||
|
// body: { register_token, password, confirm_password }
|
||||||
|
// 密码最少 6 位;复用 CompleteRegister 建号 + 绑定微信,随后签发 tid=0 正式令牌。
|
||||||
|
func (c *OnboardingController) Register() {
|
||||||
|
var req struct {
|
||||||
|
Token string `json:"register_token"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Confirm string `json:"confirm_password"`
|
||||||
|
}
|
||||||
|
if !c.readJSON(&req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tk := strings.TrimSpace(req.Token)
|
||||||
|
if tk == "" {
|
||||||
|
c.fail(400, "注册会话已失效,请重新授权手机号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pwd := strings.TrimSpace(req.Password)
|
||||||
|
if len(pwd) < 6 {
|
||||||
|
c.fail(400, "密码长度不能少于6位")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pwd != strings.TrimSpace(req.Confirm) {
|
||||||
|
c.fail(400, "两次输入的密码不一致")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashed, herr := passwordutil.Hash(pwd)
|
||||||
|
if herr != nil {
|
||||||
|
c.fail(500, "密码处理失败,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var session models.AuthRegisterSession
|
||||||
|
if err := models.Orm.QueryTable(new(models.AuthRegisterSession)).Filter("token", tk).One(&session); err != nil {
|
||||||
|
c.fail(400, "注册会话已失效,请重新授权手机号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if time.Now().After(session.ExpireAt) {
|
||||||
|
c.fail(400, "注册会话已过期,请重新授权手机号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
num, uerr := models.Orm.QueryTable(new(models.AuthRegisterSession)).
|
||||||
|
Filter("token", tk).
|
||||||
|
Update(map[string]interface{}{
|
||||||
|
"password_hash": hashed,
|
||||||
|
"password_algo": passwordutil.AlgoOf(hashed),
|
||||||
|
"update_time": time.Now(),
|
||||||
|
})
|
||||||
|
if uerr != nil || num == 0 {
|
||||||
|
c.fail(400, "注册会话已失效,请重新授权手机号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityID, err := services.CompleteRegister(tk)
|
||||||
|
if err != nil {
|
||||||
|
c.fail(400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 注册会话一次性使用:账号已建,后续申请凭 Bearer 令牌
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.AuthRegisterSession)).Filter("token", tk).Delete()
|
||||||
|
|
||||||
|
tokens, terr := c.issueNoTenantTokens(identityID, session.Mobile)
|
||||||
|
if terr != nil {
|
||||||
|
logs.Error("[onboarding] 签发无企业令牌失败: %v", terr)
|
||||||
|
c.fail(500, "注册成功但登录态签发失败,请重新进入小程序")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{
|
||||||
|
"tokens": tokens,
|
||||||
|
"need_tenant": true,
|
||||||
|
"mobile_mask": maskPhone(session.Mobile),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// issueNoTenantTokens 注册即登录:签发 tid=0、userType=app 的正式令牌。
|
||||||
|
// /api/home 对 tid=0 返回公开模块与公开菜单,因此原「免登录」功能可直接使用。
|
||||||
|
func (c *OnboardingController) issueNoTenantTokens(identityID uint64, mobile string) (*authsvc.TokenPair, error) {
|
||||||
|
sess, err := authsvc.CreateSession(authsvc.SessionInfo{
|
||||||
|
IdentityID: identityID,
|
||||||
|
Tid: 0,
|
||||||
|
ClientID: onboardingClientID,
|
||||||
|
IP: c.Ctx.Input.IP(),
|
||||||
|
UserAgent: c.Ctx.Request.UserAgent(),
|
||||||
|
LoginType: "wechat_phone",
|
||||||
|
Amr: "wechat_phone",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return authsvc.IssueTokens(authsvc.TokenIssue{
|
||||||
|
IdentityID: identityID,
|
||||||
|
Tid: 0,
|
||||||
|
ClientID: onboardingClientID,
|
||||||
|
Sid: sess.Sid,
|
||||||
|
Username: maskPhone(mobile),
|
||||||
|
UserType: "app",
|
||||||
|
Amr: "wechat_phone",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status GET /api/onboarding/status
|
||||||
|
// 返回企业绑定、最新申请状态、微信手机号(前端只读展示)。
|
||||||
|
func (c *OnboardingController) Status() {
|
||||||
|
cl := c.mustClaims()
|
||||||
|
if cl == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
identityID := uint64(cl.UserID)
|
||||||
|
|
||||||
|
var ai models.AuthIdentity
|
||||||
|
mobile := ""
|
||||||
|
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||||
|
Filter("id", identityID).Filter("delete_time__isnull", true).One(&ai); err == nil && ai.Mobile != nil {
|
||||||
|
mobile = *ai.Mobile
|
||||||
|
}
|
||||||
|
|
||||||
|
var binds []models.AuthTenantUser
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||||
|
Filter("identity_id", identityID).
|
||||||
|
Filter("status", 1).
|
||||||
|
All(&binds)
|
||||||
|
hasTenant := len(binds) > 0
|
||||||
|
tenantName := ""
|
||||||
|
tid := uint64(0)
|
||||||
|
if hasTenant {
|
||||||
|
tid = binds[0].Tid
|
||||||
|
var t models.SystemTenant
|
||||||
|
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&t); err == nil {
|
||||||
|
tenantName = t.TenantName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var applyData interface{}
|
||||||
|
var apply models.AuthTenantApply
|
||||||
|
if err := models.Orm.QueryTable(new(models.AuthTenantApply)).
|
||||||
|
Filter("identity_id", identityID).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
OrderBy("-id").One(&apply); err == nil {
|
||||||
|
applyData = map[string]interface{}{
|
||||||
|
"id": apply.ID,
|
||||||
|
"type": apply.Type,
|
||||||
|
"status": apply.Status,
|
||||||
|
"tenant_name": apply.TenantName,
|
||||||
|
"reject_reason": apply.RejectReason,
|
||||||
|
"department_name": apply.DepartmentName,
|
||||||
|
"position_name": apply.PositionName,
|
||||||
|
"applicant_name": apply.ApplicantName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ok(map[string]interface{}{
|
||||||
|
"has_tenant": hasTenant,
|
||||||
|
"tid": tid,
|
||||||
|
"tenant_name": tenantName,
|
||||||
|
"mobile": mobile,
|
||||||
|
"mobile_mask": maskPhone(mobile),
|
||||||
|
"apply": applyData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenants GET /api/onboarding/tenants?keyword=xxx
|
||||||
|
// 挂靠企业:按企业全称搜索启用中的企业(复用注册页的企业联想)。
|
||||||
|
func (c *OnboardingController) Tenants() {
|
||||||
|
items, err := services.SearchTenantsForRegister(c.GetString("keyword"))
|
||||||
|
if err != nil {
|
||||||
|
c.fail(500, "企业搜索失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{"list": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Departments GET /api/onboarding/departments?tid=xxx
|
||||||
|
// 公开下拉:按企业返回组织(部门)列表。挂靠时用户尚未加入该企业,故不走 /backend 鉴权接口。
|
||||||
|
func (c *OnboardingController) Departments() {
|
||||||
|
tid := c.uintParam("tid")
|
||||||
|
if tid == 0 {
|
||||||
|
c.fail(400, "请先选择企业")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rows []models.BackendOrganization
|
||||||
|
_, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||||
|
Filter("tid", tid).
|
||||||
|
Filter("status", 1).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
OrderBy("sort", "id").
|
||||||
|
All(&rows)
|
||||||
|
if err != nil {
|
||||||
|
c.fail(500, "部门列表读取失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
list = append(list, map[string]interface{}{
|
||||||
|
"id": r.ID, "name": r.OrgName, "parent_id": r.ParentID, "is_company": r.IsCompany,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{"list": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positions GET /api/onboarding/positions?tid=xxx&department_id=xxx
|
||||||
|
func (c *OnboardingController) Positions() {
|
||||||
|
tid := c.uintParam("tid")
|
||||||
|
if tid == 0 {
|
||||||
|
c.fail(400, "请先选择企业")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qs := models.Orm.QueryTable(new(models.BackendPosition)).
|
||||||
|
Filter("tid", tid).
|
||||||
|
Filter("status", 1).
|
||||||
|
Filter("delete_time__isnull", true)
|
||||||
|
if deptID := c.uintParam("department_id"); deptID > 0 {
|
||||||
|
qs = qs.Filter("department_id", deptID)
|
||||||
|
}
|
||||||
|
var rows []models.BackendPosition
|
||||||
|
if _, err := qs.OrderBy("sort", "id").All(&rows); err != nil {
|
||||||
|
c.fail(500, "职位列表读取失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
list = append(list, map[string]interface{}{
|
||||||
|
"id": r.ID, "name": r.PositionName, "department_id": r.DepartmentID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{"list": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyJoin POST /api/onboarding/apply-join
|
||||||
|
// body: { tid, applicant_name, department_id, department_name, position_id, position_name }
|
||||||
|
// 手机号不接受前端传值:由后端从微信验证过的身份手机号写入 wechat_mobile。
|
||||||
|
func (c *OnboardingController) ApplyJoin() {
|
||||||
|
cl := c.mustClaims()
|
||||||
|
if cl == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.hasTenant(uint64(cl.UserID)) {
|
||||||
|
c.fail(400, "你已加入企业,无需重复申请")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req struct {
|
||||||
|
Tid uint64 `json:"tid"`
|
||||||
|
ApplicantName string `json:"applicant_name"`
|
||||||
|
DepartmentID uint64 `json:"department_id"`
|
||||||
|
DepartmentName string `json:"department_name"`
|
||||||
|
PositionID uint64 `json:"position_id"`
|
||||||
|
PositionName string `json:"position_name"`
|
||||||
|
}
|
||||||
|
if !c.readJSON(&req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Tid == 0 || strings.TrimSpace(req.ApplicantName) == "" || req.DepartmentID == 0 || req.PositionID == 0 {
|
||||||
|
c.fail(400, "请填写姓名并选择企业、部门、职位")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mobile := c.identityMobile(uint64(cl.UserID))
|
||||||
|
apply, err := services.CreateJoinApply(uint64(cl.UserID), req.Tid, services.JoinApplyDetail{
|
||||||
|
ApplicantName: req.ApplicantName,
|
||||||
|
DepartmentID: req.DepartmentID,
|
||||||
|
DepartmentName: req.DepartmentName,
|
||||||
|
PositionID: req.PositionID,
|
||||||
|
PositionName: req.PositionName,
|
||||||
|
WechatMobile: mobile,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.fail(400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 微信手机号单独落库,避免被表单联系人字段覆盖
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.AuthTenantApply)).
|
||||||
|
Filter("id", apply.ID).
|
||||||
|
Update(map[string]interface{}{"wechat_mobile": mobile})
|
||||||
|
c.ok(map[string]interface{}{"apply_id": apply.ID, "status": apply.Status})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyCreate POST /api/onboarding/apply-create
|
||||||
|
// 沿用 PC 注册页的完整字段与校验(信用代码 18 位、营业执照、法人、联系人、电话、邮箱必填)。
|
||||||
|
func (c *OnboardingController) ApplyCreate() {
|
||||||
|
cl := c.mustClaims()
|
||||||
|
if cl == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.hasTenant(uint64(cl.UserID)) {
|
||||||
|
c.fail(400, "你已加入企业,无需重复申请")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req services.TenantCreateApplyInput
|
||||||
|
if !c.readJSON(&req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apply, err := services.CreateTenantApply(uint64(cl.UserID), req)
|
||||||
|
if err != nil {
|
||||||
|
c.fail(400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{"apply_id": apply.ID, "status": apply.Status})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) hasTenant(identityID uint64) bool {
|
||||||
|
cnt, _ := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||||
|
Filter("identity_id", identityID).
|
||||||
|
Filter("status", 1).
|
||||||
|
Count()
|
||||||
|
return cnt > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) identityMobile(identityID uint64) string {
|
||||||
|
var ai models.AuthIdentity
|
||||||
|
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||||
|
Filter("id", identityID).Filter("delete_time__isnull", true).One(&ai); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if ai.Mobile == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *ai.Mobile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OnboardingController) uintParam(key string) uint64 {
|
||||||
|
v, _ := strconv.ParseUint(strings.TrimSpace(c.GetString(key)), 10, 64)
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -33,7 +33,6 @@ func (c *PlatformWebsiteSettingController) jsonErr(httpStatus, bizCode int, msg
|
|||||||
// Info GET /platform/website/setting/info
|
// Info GET /platform/website/setting/info
|
||||||
// 直接把表里所有设置项返回,前端按行渲染(新增设置项无需改代码):
|
// 直接把表里所有设置项返回,前端按行渲染(新增设置项无需改代码):
|
||||||
// data.list = [{ id, title, value, tag, remark }]
|
// data.list = [{ id, title, value, tag, remark }]
|
||||||
// data.values = { "sitename": "云泽科技", ... }(key 即标签名,方便直接使用)
|
|
||||||
func (c *PlatformWebsiteSettingController) Info() {
|
func (c *PlatformWebsiteSettingController) Info() {
|
||||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||||
code := identity.Status(err)
|
code := identity.Status(err)
|
||||||
@@ -65,7 +64,6 @@ func (c *PlatformWebsiteSettingController) Info() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save POST /platform/website/setting/save
|
// Save POST /platform/website/setting/save
|
||||||
// 入参 { "sitename": "云泽科技", "subtitle": "...", ... },key 为标签名,按标签逐项 upsert。
|
|
||||||
// 通用:表里已存在或新增的任意合法标签都可写入,不需要登记。
|
// 通用:表里已存在或新增的任意合法标签都可写入,不需要登记。
|
||||||
func (c *PlatformWebsiteSettingController) Save() {
|
func (c *PlatformWebsiteSettingController) Save() {
|
||||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 小程序「挂靠企业」申请表补列
|
||||||
|
-- 前置:yz_auth_tenant_apply 已由 create_auth_tenant_apply.sql 创建。
|
||||||
|
-- 幂等说明:MySQL 5.7/8.0 不支持 ADD COLUMN IF NOT EXISTS,
|
||||||
|
-- 重复执行会报 Duplicate column,可忽略(先 SELECT 确认)。
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE `yz_auth_tenant_apply`
|
||||||
|
ADD COLUMN `applicant_name` varchar(64) NOT NULL DEFAULT '' COMMENT '申请人姓名(挂靠表单填写)' AFTER `identity_id`,
|
||||||
|
ADD COLUMN `department_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '部门ID(yz_backend_organization.id)' AFTER `applicant_name`,
|
||||||
|
ADD COLUMN `department_name` varchar(128) NOT NULL DEFAULT '' COMMENT '部门名称快照' AFTER `department_id`,
|
||||||
|
ADD COLUMN `position_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '职位ID(yz_backend_position.id)' AFTER `department_name`,
|
||||||
|
ADD COLUMN `position_name` varchar(128) NOT NULL DEFAULT '' COMMENT '职位名称快照' AFTER `position_id`,
|
||||||
|
ADD COLUMN `wechat_mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '微信验证手机号(只读,后端写入)' AFTER `position_name`;
|
||||||
@@ -84,5 +84,5 @@ WHERE @notice_pid IS NOT NULL
|
|||||||
INSERT INTO `yz_platform_website_notice`
|
INSERT INTO `yz_platform_website_notice`
|
||||||
(`title`, `summary`, `content`, `type`, `is_popup`, `top`, `sort`, `status`, `publish_time`)
|
(`title`, `summary`, `content`, `type`, `is_popup`, `top`, `sort`, `status`, `publish_time`)
|
||||||
VALUES
|
VALUES
|
||||||
('云泽科技官网全新改版上线', '本次改版聚焦信息架构与访问体验,欢迎反馈建议。',
|
('温新云科技官网全新改版上线', '本次改版聚焦信息架构与访问体验,欢迎反馈建议。',
|
||||||
'<p>云泽科技官网全新改版上线,本次改版聚焦信息架构与访问体验。</p>', 1, 1, 1, 0, 1, NOW());
|
'<p>温新云科技官网全新改版上线,本次改版聚焦信息架构与访问体验。</p>', 1, 1, 1, 0, 1, NOW());
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
merge_key,row_count,password_variants,tids,representative_id
|
|
||||||
m:19895983967,2,2,1|2,4
|
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
merge_key,row_count,password_variants,tids,representative_id
|
|
||||||
m:19895983967,2,2,1|2,4
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
old_id,old_uid,tid,identity_id,tenant_user_id,account,merge_key,conflict
|
|
||||||
2,96085169,2,1,1,test1,m:13357867407,false
|
|
||||||
1,88888888,1,2,2,hero920103,m:19895983967,true
|
|
||||||
4,67091493,2,2,3,hero920103,m:19895983967,true
|
|
||||||
|
@@ -22,6 +22,13 @@ const (
|
|||||||
type AuthTenantApply struct {
|
type AuthTenantApply struct {
|
||||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
IdentityID uint64 `orm:"column(identity_id)" json:"identity_id"`
|
IdentityID uint64 `orm:"column(identity_id)" json:"identity_id"`
|
||||||
|
// 小程序「挂靠企业」填写的资料:姓名 / 部门 / 职位 / 微信手机号(只读,后端取身份手机号)
|
||||||
|
ApplicantName string `orm:"column(applicant_name);size(64);default('')" json:"applicant_name"`
|
||||||
|
DepartmentID uint64 `orm:"column(department_id);default(0)" json:"department_id"`
|
||||||
|
DepartmentName string `orm:"column(department_name);size(128);default('')" json:"department_name"`
|
||||||
|
PositionID uint64 `orm:"column(position_id);default(0)" json:"position_id"`
|
||||||
|
PositionName string `orm:"column(position_name);size(128);default('')" json:"position_name"`
|
||||||
|
WechatMobile string `orm:"column(wechat_mobile);size(20);default('')" json:"wechat_mobile"`
|
||||||
Type int8 `orm:"column(type);default(1)" json:"type"`
|
Type int8 `orm:"column(type);default(1)" json:"type"`
|
||||||
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
|
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
|
||||||
TenantName string `orm:"column(tenant_name);size(128);default('')" json:"tenant_name"`
|
TenantName string `orm:"column(tenant_name);size(128);default('')" json:"tenant_name"`
|
||||||
|
|||||||
Binary file not shown.
@@ -185,4 +185,16 @@ func Register() {
|
|||||||
beego.Router("/api/guest/data/:id", &controllers.GuestController{}, "put:DataUpdate;delete:DataDelete")
|
beego.Router("/api/guest/data/:id", &controllers.GuestController{}, "put:DataUpdate;delete:DataDelete")
|
||||||
// 移动端用户自定义「常用功能」保存(读取随 /api/home 下发 quick_menu_ids)
|
// 移动端用户自定义「常用功能」保存(读取随 /api/home 下发 quick_menu_ids)
|
||||||
beego.Router("/api/quick-menus", &controllers.AppAggregateController{}, "post:SaveQuickMenus")
|
beego.Router("/api/quick-menus", &controllers.AppAggregateController{}, "post:SaveQuickMenus")
|
||||||
|
|
||||||
|
// ===== 小程序新用户引导:手机号 → 设密码注册 → 创建 / 挂靠企业 =====
|
||||||
|
// 公开路径(不在 jwt 保护前缀内):phone / register 由微信凭证与注册票据自证,
|
||||||
|
// 其余端点由控制器自行校验 Bearer 令牌(含 tid=0 的未加入企业令牌)。
|
||||||
|
beego.Router("/api/onboarding/phone", &controllers.OnboardingController{}, "post:Phone")
|
||||||
|
beego.Router("/api/onboarding/register", &controllers.OnboardingController{}, "post:Register")
|
||||||
|
beego.Router("/api/onboarding/status", &controllers.OnboardingController{}, "get:Status")
|
||||||
|
beego.Router("/api/onboarding/tenants", &controllers.OnboardingController{}, "get:Tenants")
|
||||||
|
beego.Router("/api/onboarding/departments", &controllers.OnboardingController{}, "get:Departments")
|
||||||
|
beego.Router("/api/onboarding/positions", &controllers.OnboardingController{}, "get:Positions")
|
||||||
|
beego.Router("/api/onboarding/apply-join", &controllers.OnboardingController{}, "post:ApplyJoin")
|
||||||
|
beego.Router("/api/onboarding/apply-create", &controllers.OnboardingController{}, "post:ApplyCreate")
|
||||||
}
|
}
|
||||||
@@ -436,8 +436,20 @@ func SearchTenantsForRegister(keyword string) ([]TenantSearchItem, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JoinApplyDetail 小程序「挂靠企业」补充资料。
|
||||||
|
// 手机号不允许前端传:统一由后端从身份表读取微信验证过的手机号。
|
||||||
|
type JoinApplyDetail struct {
|
||||||
|
ApplicantName string
|
||||||
|
DepartmentID uint64
|
||||||
|
DepartmentName string
|
||||||
|
PositionID uint64
|
||||||
|
PositionName string
|
||||||
|
WechatMobile string
|
||||||
|
}
|
||||||
|
|
||||||
// CreateJoinApply 「绑定企业」申请:等待目标企业管理员审批
|
// CreateJoinApply 「绑定企业」申请:等待目标企业管理员审批
|
||||||
func CreateJoinApply(identityID, tid uint64) (*models.AuthTenantApply, error) {
|
// detail 为可选参数,PC 两步式注册沿用原调用(不传),小程序挂靠表单传入补充资料。
|
||||||
|
func CreateJoinApply(identityID, tid uint64, detail ...JoinApplyDetail) (*models.AuthTenantApply, error) {
|
||||||
if identityID == 0 || tid == 0 {
|
if identityID == 0 || tid == 0 {
|
||||||
return nil, errors.New("参数错误")
|
return nil, errors.New("参数错误")
|
||||||
}
|
}
|
||||||
@@ -477,7 +489,22 @@ func CreateJoinApply(identityID, tid uint64) (*models.AuthTenantApply, error) {
|
|||||||
TenantName: tenant.TenantName,
|
TenantName: tenant.TenantName,
|
||||||
Status: models.TenantApplyStatusPending,
|
Status: models.TenantApplyStatusPending,
|
||||||
}
|
}
|
||||||
|
// 小程序挂靠表单:补充姓名 / 部门 / 职位,手机号取微信验证过的身份手机号
|
||||||
|
if len(detail) > 0 {
|
||||||
|
d := detail[0]
|
||||||
|
apply.ApplicantName = strings.TrimSpace(d.ApplicantName)
|
||||||
|
apply.DepartmentID = d.DepartmentID
|
||||||
|
apply.DepartmentName = strings.TrimSpace(d.DepartmentName)
|
||||||
|
apply.PositionID = d.PositionID
|
||||||
|
apply.PositionName = strings.TrimSpace(d.PositionName)
|
||||||
|
apply.WechatMobile = strings.TrimSpace(d.WechatMobile)
|
||||||
|
if apply.ApplicantName != "" {
|
||||||
|
apply.ContactPerson = apply.ApplicantName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if apply.ContactPerson == "" {
|
||||||
apply.ContactPerson = identityNickname(identity)
|
apply.ContactPerson = identityNickname(identity)
|
||||||
|
}
|
||||||
if identity.Mobile != nil {
|
if identity.Mobile != nil {
|
||||||
apply.ContactPhone = *identity.Mobile
|
apply.ContactPhone = *identity.Mobile
|
||||||
}
|
}
|
||||||
@@ -622,6 +649,13 @@ func ApproveJoinApply(applyID, tid, reviewerID uint64, reviewerName string) erro
|
|||||||
if err := bindIdentityToTenant(identity, tid, models.GetTenantUserRole); err != nil {
|
if err := bindIdentityToTenant(identity, tid, models.GetTenantUserRole); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 挂靠申请选了部门:审批通过后把 org_id 落到认证中心绑定行
|
||||||
|
if apply.DepartmentID > 0 {
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||||
|
Filter("tid", tid).
|
||||||
|
Filter("identity_id", apply.IdentityID).
|
||||||
|
Update(map[string]interface{}{"org_id": apply.DepartmentID, "update_time": time.Now()})
|
||||||
|
}
|
||||||
return finishApply(apply, models.TenantApplyStatusPassed, "", reviewerID, reviewerName)
|
return finishApply(apply, models.TenantApplyStatusPassed, "", reviewerID, reviewerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ func (s *WechatMpSender) Send(ctx context.Context, reminder ReminderData, title,
|
|||||||
perr := wechatmp.PushToOpenID(cfg, openid, wechatmp.PushMessage{
|
perr := wechatmp.PushToOpenID(cfg, openid, wechatmp.PushMessage{
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
Remark: "来自云泽管理平台日程提醒",
|
Remark: "来自温新云平台日程提醒",
|
||||||
})
|
})
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return false, perr
|
return false, perr
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ func (e *APIError) Explain() string {
|
|||||||
return "AppID 无效:请核对小程序后台「开发 → 开发管理」中的 AppID"
|
return "AppID 无效:请核对小程序后台「开发 → 开发管理」中的 AppID"
|
||||||
case 40125:
|
case 40125:
|
||||||
return "AppSecret 错误:请在小程序后台重置后复制新的 AppSecret 填入本页"
|
return "AppSecret 错误:请在小程序后台重置后复制新的 AppSecret 填入本页"
|
||||||
|
case 40164, 61004:
|
||||||
|
return "服务器出口 IP 不在白名单:请在小程序后台「开发管理 → 开发设置 → IP白名单」加入本服务器出口 IP"
|
||||||
case 40226:
|
case 40226:
|
||||||
return "该小程序未开通手机号快速验证能力:请在「开发管理 → 接口设置」中开通"
|
return "该小程序未开通手机号快速验证能力:请在「开发管理 → 接口设置」中开通"
|
||||||
case 45009:
|
case 45009:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -187,7 +187,7 @@
|
|||||||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="brand-name">云泽统一认证中心</span>
|
<span class="brand-name">温新云统一认证中心</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="side-title">找回登录密码</div>
|
<div class="side-title">找回登录密码</div>
|
||||||
|
|||||||
@@ -560,7 +560,7 @@
|
|||||||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="brand-name">云泽统一认证中心</span>
|
<span class="brand-name">温新云统一认证中心</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -592,7 +592,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="mobile-brand-text">
|
<span class="mobile-brand-text">
|
||||||
<span class="mobile-brand-name">云泽统一认证中心</span>
|
<span class="mobile-brand-name">温新云统一认证中心</span>
|
||||||
<span class="mobile-brand-sub">{{.LoginTitle}}</span>
|
<span class="mobile-brand-sub">{{.LoginTitle}}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -769,7 +769,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="qr-title">微信扫码登录</p>
|
<p class="qr-title">微信扫码登录</p>
|
||||||
<p class="qr-desc">使用微信「扫一扫」,扫码后在<strong>云泽小程序</strong>中点击确认<br />即可完成登录</p>
|
<p class="qr-desc">使用微信「扫一扫」,扫码后在<strong>温新云小程序</strong>中点击确认<br />即可完成登录</p>
|
||||||
<p class="qr-note">二维码 5 分钟内有效,过期可点击刷新</p>
|
<p class="qr-note">二维码 5 分钟内有效,过期可点击刷新</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -799,7 +799,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-foot">
|
<div class="form-foot">
|
||||||
<span>© 2026 云泽科技 版权所有</span>
|
<span>© 2026 云泽 版权所有</span>
|
||||||
<button id="cacheClearBtn" class="cache-clear-btn" type="button" onclick="clearSiteCache()" title="仅清除本站在当前浏览器中的缓存数据">清除缓存</button>
|
<button id="cacheClearBtn" class="cache-clear-btn" type="button" onclick="clearSiteCache()" title="仅清除本站在当前浏览器中的缓存数据">清除缓存</button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- 系统基本信息 -->
|
<!-- 系统基本信息 -->
|
||||||
<el-descriptions title="系统基本信息" :column="2" border class="info-section">
|
<el-descriptions title="系统基本信息" :column="2" border class="info-section">
|
||||||
<el-descriptions-item label="系统名称">
|
<el-descriptions-item label="系统名称">
|
||||||
<el-tag type="primary">{{ systemInfo.name || '云泽管理系统' }}</el-tag>
|
<el-tag type="primary">{{ systemInfo.name || '温新云平台' }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="系统版本">
|
<el-descriptions-item label="系统版本">
|
||||||
<el-tag>{{ systemInfo.version || '1.0.0' }}</el-tag>
|
<el-tag>{{ systemInfo.version || '1.0.0' }}</el-tag>
|
||||||
@@ -226,7 +226,7 @@ async function fetchSystemInfo() {
|
|||||||
|
|
||||||
// 模拟数据(如果有API会替换)
|
// 模拟数据(如果有API会替换)
|
||||||
systemInfo.value = {
|
systemInfo.value = {
|
||||||
name: '云泽管理系统',
|
name: '温新云平台',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
environment: 'Production',
|
environment: 'Production',
|
||||||
uptime: '24天 5小时 30分钟',
|
uptime: '24天 5小时 30分钟',
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ const totalTokens = computed(() =>
|
|||||||
const scenes = [
|
const scenes = [
|
||||||
{ label: "通用问答", prompt: "用三句话介绍一下你自己" },
|
{ label: "通用问答", prompt: "用三句话介绍一下你自己" },
|
||||||
{ label: "代码生成", prompt: "用 Vue3 写一个防抖函数示例" },
|
{ label: "代码生成", prompt: "用 Vue3 写一个防抖函数示例" },
|
||||||
{ label: "文案翻译", prompt: "把「欢迎使用云泽科技」翻译成英文和日文" },
|
{ label: "文案翻译", prompt: "把「欢迎使用温新云科技」翻译成英文和日文" },
|
||||||
{ label: "OCR 提示词", prompt: "请把图片中的文字完整识别出来,保持原有排版" },
|
{ label: "OCR 提示词", prompt: "请把图片中的文字完整识别出来,保持原有排版" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
class="preview-logo"
|
class="preview-logo"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
/>
|
/>
|
||||||
<div v-else class="preview-logo-fallback">云泽统一认证中心</div>
|
<div v-else class="preview-logo-fallback">温新云统一认证中心</div>
|
||||||
<p class="preview-title">{{ form.login_title || "一次登录,全平台通行" }}</p>
|
<p class="preview-title">{{ form.login_title || "一次登录,全平台通行" }}</p>
|
||||||
<p class="preview-sub">
|
<p class="preview-sub">
|
||||||
{{ form.login_subtitle || "基于 OIDC / OAuth 2.0 的统一身份底座" }}
|
{{ form.login_subtitle || "基于 OIDC / OAuth 2.0 的统一身份底座" }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<el-form-item label="扫码登录">
|
<el-form-item label="扫码登录">
|
||||||
<el-switch v-model="form.login_qr_enabled" active-value="1" inactive-value="0" />
|
<el-switch v-model="form.login_qr_enabled" active-value="1" inactive-value="0" />
|
||||||
<div class="form-tip">微信扫码登录(云泽小程序内确认)。关闭后登录页不再显示该 tab。</div>
|
<div class="form-tip">微信扫码登录(温新云小程序内确认)。关闭后登录页不再显示该 tab。</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
<el-input v-model="formData.sms.secret_key" type="password" show-password placeholder="请输入 AccessKey Secret" />
|
<el-input v-model="formData.sms.secret_key" type="password" show-password placeholder="请输入 AccessKey Secret" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="短信签名">
|
<el-form-item label="短信签名">
|
||||||
<el-input v-model="formData.sms.sign_name" placeholder="例如:云泽科技" />
|
<el-input v-model="formData.sms.sign_name" placeholder="例如:温新科技" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="短信模板 Code">
|
<el-form-item label="短信模板 Code">
|
||||||
<el-input v-model="formData.sms.template_code" placeholder="例如:SMS_200300400" />
|
<el-input v-model="formData.sms.template_code" placeholder="例如:SMS_200300400" />
|
||||||
|
|||||||
@@ -68,6 +68,15 @@
|
|||||||
<div class="sub-text">{{ scope.row.identity_mobile || "" }}</div>
|
<div class="sub-text">{{ scope.row.identity_mobile || "" }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="挂靠资料" min-width="170" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<div v-if="scope.row.type === 1">{{ scope.row.applicant_name || "-" }}</div>
|
||||||
|
<div class="sub-text">
|
||||||
|
{{ scope.row.department_name || "-" }} · {{ scope.row.position_name || "-" }}
|
||||||
|
</div>
|
||||||
|
<div class="sub-text">微信手机号 {{ scope.row.wechat_mobile || "-" }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="tenant_name"
|
prop="tenant_name"
|
||||||
label="企业名称"
|
label="企业名称"
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 小程序新用户引导:手机号授权 → 密码注册 → 创建 / 挂靠企业。
|
||||||
|
* 统一走 /api/onboarding/*,其余业务接口不变。
|
||||||
|
*/
|
||||||
|
import { request } from '@/api/request.js'
|
||||||
|
|
||||||
|
/** 微信手机号授权:返回 { exists, need_register, register_token, mobile_mask } */
|
||||||
|
export function onboardingPhone({ code, phoneCode, appid = '' } = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/onboarding/phone',
|
||||||
|
method: 'POST',
|
||||||
|
data: { code, phone_code: phoneCode, appid },
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 无账号时设置密码注册(≥6 位),返回 { tokens, need_tenant, mobile_mask } */
|
||||||
|
export function onboardingRegister({ token, password, confirmPassword }) {
|
||||||
|
return request({
|
||||||
|
url: '/api/onboarding/register',
|
||||||
|
method: 'POST',
|
||||||
|
data: { register_token: token, password, confirm_password: confirmPassword },
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 企业绑定 / 最新申请状态 / 微信手机号(手机号只读) */
|
||||||
|
export function getOnboardingStatus() {
|
||||||
|
return request({ url: '/api/onboarding/status', method: 'GET', silent: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 挂靠企业:按企业全称搜索 */
|
||||||
|
export function searchOnboardingTenants(keyword) {
|
||||||
|
return request({
|
||||||
|
url: '/api/onboarding/tenants',
|
||||||
|
method: 'GET',
|
||||||
|
data: { keyword },
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 挂靠企业:按企业取部门下拉 */
|
||||||
|
export function getOnboardingDepartments(tid) {
|
||||||
|
return request({
|
||||||
|
url: '/api/onboarding/departments',
|
||||||
|
method: 'GET',
|
||||||
|
data: { tid },
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 挂靠企业:按企业 + 部门取职位下拉 */
|
||||||
|
export function getOnboardingPositions(tid, departmentId) {
|
||||||
|
return request({
|
||||||
|
url: '/api/onboarding/positions',
|
||||||
|
method: 'GET',
|
||||||
|
data: { tid, department_id: departmentId },
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交挂靠企业申请(手机号由后端取微信验证结果,禁止前端传) */
|
||||||
|
export function applyJoin(data) {
|
||||||
|
return request({ url: '/api/onboarding/apply-join', method: 'POST', data, silent: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交创建企业申请(沿用 PC 完整字段与校验) */
|
||||||
|
export function applyCreate(data) {
|
||||||
|
return request({ url: '/api/onboarding/apply-create', method: 'POST', data, silent: true })
|
||||||
|
}
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
<text class="avatar-text">{{ avatarText }}</text>
|
<text class="avatar-text">{{ avatarText }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-meta">
|
<view class="user-meta">
|
||||||
<text class="user-name">{{ user.name || '云泽用户' }}</text>
|
<text class="user-name">{{ user.name || '温新云用户' }}</text>
|
||||||
<text class="user-sub">{{ user.tenant || '云泽平台' }}<text v-if="user.account"> · {{ user.account }}</text></text>
|
<text class="user-sub">{{ user.tenant || '温新云平台' }}<text v-if="user.account"> · {{ user.account }}</text></text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
<text class="avatar-text avatar-text-dark">{{ avatarText }}</text>
|
<text class="avatar-text avatar-text-dark">{{ avatarText }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-meta">
|
<view class="user-meta">
|
||||||
<text class="user-name user-name-dark">{{ user.name || '云泽用户' }}</text>
|
<text class="user-name user-name-dark">{{ user.name || '温新云用户' }}</text>
|
||||||
<text class="user-sub">{{ user.tenant || '云泽平台' }}<text v-if="user.account"> · {{ user.account }}</text></text>
|
<text class="user-sub">{{ user.tenant || '温新云平台' }}<text v-if="user.account"> · {{ user.account }}</text></text>
|
||||||
</view>
|
</view>
|
||||||
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
|
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"vueVersion" : "3",
|
"vueVersion" : "3",
|
||||||
"h5" : {
|
"h5" : {
|
||||||
"title" : "云泽系统",
|
"title" : "温新云科技",
|
||||||
"router" : {
|
"router" : {
|
||||||
"mode" : "hash",
|
"mode" : "hash",
|
||||||
"base" : "/h5/"
|
"base" : "/h5/"
|
||||||
|
|||||||
+15
-1
@@ -50,6 +50,20 @@
|
|||||||
"navigationBarTitleText": "确认注册"
|
"navigationBarTitleText": "确认注册"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/onboarding/register",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "设置密码"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/onboarding/company",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "加入企业"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/login/geetest-webview",
|
"path": "pages/login/geetest-webview",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -621,7 +635,7 @@
|
|||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"navigationBarTitleText": "云泽",
|
"navigationBarTitleText": "温新云",
|
||||||
"navigationBarBackgroundColor": "#F8FAFC",
|
"navigationBarBackgroundColor": "#F8FAFC",
|
||||||
"backgroundColor": "#F8FAFC",
|
"backgroundColor": "#F8FAFC",
|
||||||
"rpxCalcMaxDeviceWidth": 560,
|
"rpxCalcMaxDeviceWidth": 560,
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ const orgLine = computed(() => {
|
|||||||
if (user.value.tenant) parts.push(user.value.tenant)
|
if (user.value.tenant) parts.push(user.value.tenant)
|
||||||
if (user.value.account) parts.push(user.value.account)
|
if (user.value.account) parts.push(user.value.account)
|
||||||
if (user.value.role_name) parts.push(user.value.role_name)
|
if (user.value.role_name) parts.push(user.value.role_name)
|
||||||
return parts.length ? parts.join(' · ') : '云泽统一账号'
|
return parts.length ? parts.join(' · ') : '温新云统一账号'
|
||||||
})
|
})
|
||||||
|
|
||||||
const badgeText = computed(() => (stats.value.message > 99 ? '99+' : String(stats.value.message || '')))
|
const badgeText = computed(() => (stats.value.message > 99 ? '99+' : String(stats.value.message || '')))
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<view class="brand-logo">
|
<view class="brand-logo">
|
||||||
<FaIcon name="shield-halved" color="#ffffff" :size="22" />
|
<FaIcon name="shield-halved" color="#ffffff" :size="22" />
|
||||||
</view>
|
</view>
|
||||||
<text class="brand-name">云泽统一认证</text>
|
<text class="brand-name">温新云统一认证</text>
|
||||||
<text class="brand-desc">一次登录,全平台通行</text>
|
<text class="brand-desc">一次登录,全平台通行</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
+441
-94
@@ -4,18 +4,18 @@
|
|||||||
<view class="head" :style="headPad">
|
<view class="head" :style="headPad">
|
||||||
<view class="head-row">
|
<view class="head-row">
|
||||||
<view>
|
<view>
|
||||||
<text class="head-eyebrow">YUNZE WORKSPACE</text>
|
<text class="head-eyebrow">WENXIN WORKSPACE</text>
|
||||||
<text class="head-title">工作台</text>
|
<text class="head-title">工作台</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!guest" class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
|
<view v-if="!guest" class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
|
||||||
<view v-else class="account-chip" @tap="goLogin"><FaIcon name="right-to-bracket" color="#3478f6" :size="15" /><text>登录</text></view>
|
<view v-else class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>游客</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!guest" class="tenant-switch" @tap="chooseTenant">
|
<view v-if="!guest" class="tenant-switch" @tap="chooseTenant">
|
||||||
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
|
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
|
||||||
<view class="tenant-copy"><text class="tenant-label">当前租户</text><text class="tenant-name">{{ user.tenant || '云泽平台' }}</text></view>
|
<view class="tenant-copy"><text class="tenant-label">当前企业</text><text class="tenant-name">{{ user.tenant || '尚未加入企业' }}</text></view>
|
||||||
<view class="tenant-action"><text>切换</text><FaIcon name="chevron-down" color="#3478f6" :size="12" /></view>
|
<view class="tenant-action"><text>切换</text><FaIcon name="chevron-down" color="#3478f6" :size="12" /></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="head-sub">{{ guest ? '免登录可逛部分功能,登录后使用完整工作台' : '选择一个业务系统,开始高效工作' }}</text>
|
<!-- <text class="head-sub">{{ guest ? '免登录可逛部分功能,登录后使用完整工作台' : '选择一个业务系统,开始高效工作' }}</text> -->
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="body">
|
<view class="body">
|
||||||
@@ -36,8 +36,28 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="grid">
|
<view v-else class="grid">
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<!-- 未授权手机号:卡片本身就是微信授权按钮,点一下直接拉起手机号授权 -->
|
||||||
|
<button
|
||||||
|
v-for="card in cards"
|
||||||
|
v-show="guest"
|
||||||
|
:key="'phone-' + (card.code || card.id)"
|
||||||
|
class="cell cell-button"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
@getphonenumber="onCardPhone($event, card)"
|
||||||
|
>
|
||||||
|
<view class="portal-card" :class="{ locked: card.locked }">
|
||||||
|
<view class="tint-icon portal-icon" :class="card.tint">
|
||||||
|
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
|
||||||
|
</view>
|
||||||
|
<text class="portal-name">{{ card.name }}</text>
|
||||||
|
</view>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- 已授权(注册即登录):普通卡片,按模块是否需要企业分流 -->
|
||||||
<view
|
<view
|
||||||
v-for="card in cards"
|
v-for="card in cards"
|
||||||
|
v-show="!guest || !isMp"
|
||||||
:key="card.code || card.id"
|
:key="card.code || card.id"
|
||||||
class="cell"
|
class="cell"
|
||||||
@tap="enter(card)"
|
@tap="enter(card)"
|
||||||
@@ -47,7 +67,6 @@
|
|||||||
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
|
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
|
||||||
</view>
|
</view>
|
||||||
<text class="portal-name">{{ card.name }}</text>
|
<text class="portal-name">{{ card.name }}</text>
|
||||||
<text v-if="guest" class="portal-tag" :class="card.needLogin ? 'need' : 'free'">{{ card.needLogin ? '需登录' : '免登录' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -58,11 +77,58 @@
|
|||||||
<!-- #ifdef MP-WEIXIN -->
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
<!-- 游客无感授权:无弹窗,全屏透明按钮——首次点按即拉起微信原生手机号确认层;
|
<!-- 游客无感授权:无弹窗,全屏透明按钮——首次点按即拉起微信原生手机号确认层;
|
||||||
后端按手机号自动判定:绑定过租户→直接登录,否则→游客令牌 -->
|
后端按手机号自动判定:绑定过租户→直接登录,否则→游客令牌 -->
|
||||||
<view v-if="guestAuthVisible" class="guest-auth-mask">
|
<!-- <view v-if="guestAuthVisible" class="guest-auth-mask">
|
||||||
<button class="guest-auth-full" open-type="getPhoneNumber" :disabled="guestAuthLoading" @getphonenumber="onGuestPhone" />
|
<button class="guest-auth-full" open-type="getPhoneNumber" :disabled="guestAuthLoading" @getphonenumber="onGuestPhone" />
|
||||||
<text class="guest-auth-later" @tap="skipGuestAuth">暂不,先浏览</text>
|
<text class="guest-auth-later" @tap="skipGuestAuth">暂不,先浏览</text>
|
||||||
</view>
|
</view> -->
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<!-- 企业选择:自绘底部抽屉。原生 actionSheet 只有一行居中文本,长企业名会挤在一起,
|
||||||
|
也无法标注「当前企业」;这里给每项独立卡片 + 两行标题 + 状态标识 -->
|
||||||
|
<view v-if="tenantPickerVisible" class="tenant-picker-mask" @tap="closeTenantPicker">
|
||||||
|
<view class="tenant-picker-sheet" @tap.stop>
|
||||||
|
<view class="picker-grabber" />
|
||||||
|
<view class="picker-head">
|
||||||
|
<view class="picker-head-icon"><FaIcon name="building" color="#ffffff" :size="18" /></view>
|
||||||
|
<view class="picker-head-copy">
|
||||||
|
<text class="picker-title">{{ tenantPickerMode === 'login' ? '选择要进入的企业' : '切换企业' }}</text>
|
||||||
|
<text class="picker-sub">{{ tenantPickerMode === 'login' ? '该账号已加入多家企业,请选择工作空间' : '选择一家企业,工作台将随之刷新' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="!tenantPickerRequired" class="picker-close" @tap="closeTenantPicker">
|
||||||
|
<FaIcon name="xmark" color="#94a3b8" :size="14" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="picker-list" scroll-y :show-scrollbar="false">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in tenantPickerList"
|
||||||
|
:key="item.tid"
|
||||||
|
class="tenant-option"
|
||||||
|
:class="{ current: isCurrentTenant(item), disabled: isPickingAny() }"
|
||||||
|
@tap="pickTenant(item)"
|
||||||
|
>
|
||||||
|
<view class="tenant-option-badge" :class="'badge-' + (index % 4)">
|
||||||
|
<FaIcon name="building" color="#ffffff" :size="18" />
|
||||||
|
</view>
|
||||||
|
<view class="tenant-option-copy">
|
||||||
|
<text class="tenant-option-name">{{ item.tenant_name || '企业' + item.tid }}</text>
|
||||||
|
<text class="tenant-option-meta">{{ item.name ? item.name + ' · ' : '' }}企业 ID {{ item.tid }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="isCurrentTenant(item)" class="tenant-current-tag">当前</text>
|
||||||
|
<text v-else-if="isPickingTenant(item)" class="tenant-option-loading">进入中…</text>
|
||||||
|
<FaIcon v-else name="chevron-right" color="#cbd5e1" :size="14" />
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="picker-foot">
|
||||||
|
<view v-if="tenantPickerRequired" class="picker-hint">
|
||||||
|
<FaIcon name="circle-info" color="#94a3b8" :size="12" />
|
||||||
|
<text>选择企业后才能继续使用工作台</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="picker-cancel" @tap="closeTenantPicker"><text>取消</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -73,8 +139,10 @@ import { mpHeadPad } from '@/utils/mpHead.js'
|
|||||||
const headPad = mpHeadPad()
|
const headPad = mpHeadPad()
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { isLoggedIn, getTenants, getTid, setAuthTokens, setTenants, setTid, setTenantName, getUser, setUser } from '@/utils/auth.js'
|
import { isLoggedIn, getTenants, getTid, setAuthTokens, setTenants, setTid, setTenantName, getUser, setUser } from '@/utils/auth.js'
|
||||||
import { switchTenant } from '@/api/auth.js'
|
import { switchTenant, loginByMiniapp } from '@/api/auth.js'
|
||||||
import { fetchHome, fetchPublicModules, fetchPublicMenus } from '@/api/workspace.js'
|
import { fetchHome, fetchPublicModules, fetchPublicMenus, clearHomeCache } from '@/api/workspace.js'
|
||||||
|
import { onboardingPhone, getOnboardingStatus } from '@/api/onboarding.js'
|
||||||
|
import { setPendingCard, takePendingCard } from '@/utils/onboarding.js'
|
||||||
import { openMenu } from '@/utils/nav.js'
|
import { openMenu } from '@/utils/nav.js'
|
||||||
import { getSubsystemByModule, goSubsystem } from '@/config/subsystems.js'
|
import { getSubsystemByModule, goSubsystem } from '@/config/subsystems.js'
|
||||||
import { getGuestToken, setGuestToken, clearGuestToken } from '@/utils/guest.js'
|
import { getGuestToken, setGuestToken, clearGuestToken } from '@/utils/guest.js'
|
||||||
@@ -86,6 +154,12 @@ const guestAuthVisible = ref(false)
|
|||||||
const guestAuthLoading = ref(false)
|
const guestAuthLoading = ref(false)
|
||||||
// 本次停留内跳过:关闭后不在 nav 反复弹窗,下次冷启动再引导
|
// 本次停留内跳过:关闭后不在 nav 反复弹窗,下次冷启动再引导
|
||||||
const guestAuthDismissed = ref(false)
|
const guestAuthDismissed = ref(false)
|
||||||
|
const phoneBusy = ref(false)
|
||||||
|
// 非微信端没有 getPhoneNumber 能力:卡片退化为普通点击并给出提示
|
||||||
|
let isMp = false
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
isMp = true
|
||||||
|
// #endif
|
||||||
|
|
||||||
/** 仅微信小程序可拿手机号动态凭证 */
|
/** 仅微信小程序可拿手机号动态凭证 */
|
||||||
function canRequestGuestPhone() {
|
function canRequestGuestPhone() {
|
||||||
@@ -170,36 +244,83 @@ async function completeUserLogin(res) {
|
|||||||
uni.showToast({ title: '已登录,欢迎回来', icon: 'none' })
|
uni.showToast({ title: '已登录,欢迎回来', icon: 'none' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 待选状态下必须选一个企业才能继续,关闭选择器后再次拉起 */
|
/* ==================== 企业选择抽屉(替代 uni.showActionSheet) ==================== */
|
||||||
|
// 原生 actionSheet 只有一行居中文本:长企业名挤成一片、无法标注「当前企业」。
|
||||||
|
// 登录后的首次选择必须选一家(不可关闭),主动切换可取消,两者共用这一套抽屉。
|
||||||
|
const tenantPickerVisible = ref(false)
|
||||||
|
const tenantPickerRequired = ref(false)
|
||||||
|
const tenantPickerMode = ref('login') // login=登录后的首次选择 / switch=主动切换
|
||||||
|
const tenantPickerList = ref([])
|
||||||
|
const tenantPickingTid = ref('')
|
||||||
|
|
||||||
|
function openTenantPicker(list, mode, required) {
|
||||||
|
tenantPickerList.value = Array.isArray(list) ? list : []
|
||||||
|
tenantPickerMode.value = mode
|
||||||
|
tenantPickerRequired.value = required
|
||||||
|
tenantPickingTid.value = ''
|
||||||
|
tenantPickerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTenantPicker() {
|
||||||
|
if (tenantPickingTid.value) return
|
||||||
|
if (tenantPickerRequired.value) {
|
||||||
|
uni.showToast({ title: '请选择一家企业以继续', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tenantPickerVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCurrentTenant(item) {
|
||||||
|
return tenantPickerMode.value === 'switch' && item && String(item.tid) === String(getTid())
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPickingAny() {
|
||||||
|
return tenantPickingTid.value !== ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPickingTenant(item) {
|
||||||
|
return item && tenantPickingTid.value === String(item.tid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待选状态下必须选一家企业才能继续 */
|
||||||
function askChooseTenant(list) {
|
function askChooseTenant(list) {
|
||||||
uni.showActionSheet({
|
openTenantPicker(list, 'login', true)
|
||||||
itemList: list.map((t) => t.tenant_name || `企业${t.tid}`),
|
}
|
||||||
success: async ({ tapIndex }) => {
|
|
||||||
const tenant = list[tapIndex]
|
async function pickTenant(item) {
|
||||||
if (!tenant) return
|
if (!item || isPickingAny() || isCurrentTenant(item)) return
|
||||||
uni.showLoading({ title: '登录中...', mask: true })
|
const isSwitch = tenantPickerMode.value === 'switch'
|
||||||
|
tenantPickingTid.value = String(item.tid)
|
||||||
|
switchingTenant.value = true
|
||||||
|
uni.showLoading({ title: isSwitch ? '正在切换...' : '登录中...', mask: true })
|
||||||
try {
|
try {
|
||||||
const sw = await switchTenant({ tid: tenant.tid })
|
const res = await switchTenant({ tid: item.tid })
|
||||||
if (!sw || !sw.tokens) throw new Error((sw && sw.msg) || '进入企业失败')
|
if (!res || !res.tokens) throw new Error((res && res.msg) || (isSwitch ? '切换企业失败' : '进入企业失败'))
|
||||||
setAuthTokens(sw.tokens)
|
setAuthTokens(res.tokens)
|
||||||
setTenants(list)
|
setTenants(tenantPickerList.value)
|
||||||
setTid(tenant.tid)
|
setTid(item.tid)
|
||||||
setTenantName(tenant.tenant_name || '')
|
setTenantName(item.tenant_name || '')
|
||||||
// 先游客后转正:选定企业后移植游客数据
|
if (isSwitch) {
|
||||||
|
const profile = getUser() || {}
|
||||||
|
const nextUser = { ...profile, tid: item.tid, tenant_name: item.tenant_name || '' }
|
||||||
|
setUser(nextUser)
|
||||||
|
user.value = { ...user.value, tenant: item.tenant_name || '', account: nextUser.account || user.value.account }
|
||||||
|
}
|
||||||
|
// 成功才关闭;失败保留抽屉让用户直接重试
|
||||||
|
tenantPickerRequired.value = false
|
||||||
|
tenantPickerVisible.value = false
|
||||||
|
// 先游客后转正:企业落定后把游客数据并入正式账号
|
||||||
await mergeGuestIfAny()
|
await mergeGuestIfAny()
|
||||||
await load(true)
|
await load(true)
|
||||||
uni.showToast({ title: '已登录,欢迎回来', icon: 'none' })
|
uni.showToast({ title: isSwitch ? '已切换企业' : '已登录,欢迎回来', icon: isSwitch ? 'success' : 'none' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('[nav] switch tenant failed:', e)
|
||||||
uni.showToast({ title: (e && e.message) || '进入失败,请重试', icon: 'none' })
|
uni.showToast({ title: (e && e.message) || '进入失败,请重试', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
tenantPickingTid.value = ''
|
||||||
|
switchingTenant.value = false
|
||||||
}
|
}
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
uni.showToast({ title: '请选择要进入的企业', icon: 'none' })
|
|
||||||
setTimeout(() => askChooseTenant(list), 800)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function skipGuestAuth() {
|
function skipGuestAuth() {
|
||||||
@@ -262,17 +383,17 @@ const cards = computed(() => {
|
|||||||
if (guest.value) {
|
if (guest.value) {
|
||||||
return publicModules.value.map((mod, index) => {
|
return publicModules.value.map((mod, index) => {
|
||||||
const code = String(mod.code || '').toLowerCase()
|
const code = String(mod.code || '').toLowerCase()
|
||||||
const needLogin = !guestMenuModuleSet.value.has(code)
|
const needTenant = mod.require_login !== 0
|
||||||
return {
|
return {
|
||||||
id: mod.id,
|
id: mod.id,
|
||||||
code: mod.code,
|
code: mod.code,
|
||||||
path: mod.path || '',
|
path: mod.path || '',
|
||||||
name: mod.name || '未命名系统',
|
name: mod.name || '未命名系统',
|
||||||
desc: mod.description || (needLogin ? '需登录使用' : '免登录'),
|
desc: mod.description || (needTenant ? '加入企业后可用' : '可直接使用'),
|
||||||
icon: MODULE_ICONS[code] || 'cube',
|
icon: MODULE_ICONS[code] || 'cube',
|
||||||
tint: 'tint-' + (index % 8),
|
tint: 'tint-' + (index % 8),
|
||||||
locked: false,
|
locked: false,
|
||||||
needLogin,
|
needTenant,
|
||||||
items: [],
|
items: [],
|
||||||
total: 0
|
total: 0
|
||||||
}
|
}
|
||||||
@@ -303,6 +424,7 @@ const cards = computed(() => {
|
|||||||
icon: MODULE_ICONS[code] || 'cube',
|
icon: MODULE_ICONS[code] || 'cube',
|
||||||
tint: locked ? 'tint-off' : 'tint-' + (index % 8),
|
tint: locked ? 'tint-off' : 'tint-' + (index % 8),
|
||||||
locked,
|
locked,
|
||||||
|
needTenant: mod.require_login !== 0,
|
||||||
items,
|
items,
|
||||||
total: items.length
|
total: items.length
|
||||||
}
|
}
|
||||||
@@ -329,8 +451,7 @@ async function load(force = false) {
|
|||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
// 进小程序即引导授权:游客无令牌时弹手机号授权窗
|
// 新流程:不再自动弹授权,点任意模块卡片时才拉起微信手机号授权
|
||||||
maybeAskGuestAuth()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -352,56 +473,123 @@ async function load(force = false) {
|
|||||||
|
|
||||||
function chooseTenant() {
|
function chooseTenant() {
|
||||||
const list = tenants.value
|
const list = tenants.value
|
||||||
if (list.length <= 1) { uni.showToast({ title: list.length ? '当前账号仅绑定一个租户' : '暂无可切换的租户', icon: 'none' })
|
if (list.length <= 1) { uni.showToast({ title: list.length ? '当前账号仅绑定一个企业' : '暂无可切换的企业', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (switchingTenant.value) return
|
if (switchingTenant.value || tenantPickerVisible.value) return
|
||||||
uni.showActionSheet({
|
openTenantPicker(list, 'switch', false)
|
||||||
itemList: list.map((tenant) => tenant.tenant_name || `企业${tenant.tid}`),
|
}
|
||||||
success: async ({ tapIndex }) => {
|
|
||||||
const tenant = list[tapIndex]
|
/* ==================== 手机号授权 / 注册 / 企业分流 ==================== */
|
||||||
if (!tenant || String(tenant.tid) === String(getTid())) return
|
|
||||||
switchingTenant.value = true
|
/** 点卡片(未授权时)直接触发微信手机号授权;有账号静默登录,无账号去设密码注册 */
|
||||||
uni.showLoading({ title: '正在切换...', mask: true })
|
async function onCardPhone(e, card) {
|
||||||
|
const detail = (e && e.detail) || {}
|
||||||
|
if (String(detail.errMsg || '').indexOf('ok') === -1 || !detail.code) {
|
||||||
|
uni.showToast({ title: '需要授权手机号才能使用', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (phoneBusy.value) return
|
||||||
|
phoneBusy.value = true
|
||||||
|
uni.showLoading({ title: '验证中...', mask: true })
|
||||||
try {
|
try {
|
||||||
const res = await switchTenant({ tid: tenant.tid })
|
const code = await navUniLoginCode()
|
||||||
if (!res || !res.tokens) throw new Error((res && res.msg) || '切换租户失败')
|
let appid = ''
|
||||||
setAuthTokens(res.tokens)
|
try {
|
||||||
setTid(tenant.tid)
|
appid = (uni.getAccountInfoSync().miniProgram || {}).appId || ''
|
||||||
setTenantName(tenant.tenant_name || '')
|
} catch (err) {
|
||||||
const profile = getUser() || {}
|
appid = ''
|
||||||
const nextUser = { ...profile, tid: tenant.tid, tenant_name: tenant.tenant_name || '' }
|
}
|
||||||
setUser(nextUser)
|
setPendingCard(card)
|
||||||
user.value = { ...user.value, tenant: tenant.tenant_name || '', account: nextUser.account || user.value.account }
|
const res = await onboardingPhone({ code, phoneCode: detail.code, appid })
|
||||||
|
if (res && res.need_register) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/onboarding/register?token=' + encodeURIComponent(res.register_token || '') +
|
||||||
|
'&mobile_mask=' + encodeURIComponent(res.mobile_mask || '')
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 已有账号:复用小程序一键登录(后端按手机号认人并绑定 openid)
|
||||||
|
const ready = await silentLogin(code, detail.code)
|
||||||
|
if (!ready) return
|
||||||
await load(true)
|
await load(true)
|
||||||
uni.showToast({ title: '已切换租户', icon: 'success' })
|
await enter(card)
|
||||||
} catch (e) {
|
takePendingCard()
|
||||||
uni.showToast({ title: (e && e.message) || '切换失败,请重试', icon: 'none' })
|
} catch (err) {
|
||||||
|
console.error('[nav] onboarding phone failed:', err)
|
||||||
|
uni.showToast({ title: (err && err.message) || '授权失败,请重试', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
switchingTenant.value = false
|
phoneBusy.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
/** 已有账号静默登录;无企业时后端签发 tid=0 令牌,多企业时拉起企业选择抽屉 */
|
||||||
|
async function silentLogin(code, phoneCode) {
|
||||||
|
const res = await loginByMiniapp({ code, phoneCode })
|
||||||
|
if (!res || !res.tokens || !res.tokens.access_token) {
|
||||||
|
throw new Error((res && res.msg) || '登录失败,请重试')
|
||||||
|
}
|
||||||
|
setAuthTokens(res.tokens)
|
||||||
|
clearHomeCache()
|
||||||
|
const list = Array.isArray(res.tenants) ? res.tenants : []
|
||||||
|
setTenants(list)
|
||||||
|
if (res.need_choose_tenant && list.length > 1) {
|
||||||
|
askChooseTenant(list)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const t0 = list[0] || {}
|
||||||
|
setTid(t0.tid || 0)
|
||||||
|
setTenantName(t0.tenant_name || '')
|
||||||
|
await mergeGuestIfAny()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 点击子系统卡片 → 进入该子系统独立页面组(带自己的 tabbar)。
|
* 点击子系统卡片 → 进入该子系统独立页面组(带自己的 tabbar)。
|
||||||
* 未在 config/subsystems.js 注册的模块,降级为直接打开 Web 端该模块首页。
|
* 需企业模块:查审核状态,未通过则引导创建 / 挂靠企业;通过后刷新企业令牌再进入。
|
||||||
* 游客:命中「必须登录」模块时不直接进,先引导登录(登录后回 nav)。
|
|
||||||
*/
|
*/
|
||||||
function enter(card) {
|
async function enter(card) {
|
||||||
if (card.locked) {
|
if (card.locked) {
|
||||||
openMenu({ locked: true, title: card.name })
|
openMenu({ locked: true, title: card.name })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (guest.value && card.needLogin) {
|
if (guest.value) {
|
||||||
goLogin(card.name)
|
uni.showToast({ title: '请先授权手机号', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (card.needTenant) {
|
||||||
|
let st = null
|
||||||
|
try {
|
||||||
|
st = await getOnboardingStatus()
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: '状态获取失败,请重试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (st && st.has_tenant) {
|
||||||
|
// 审核刚通过时本地令牌可能仍是 tid=0,重新静默登录拿企业令牌
|
||||||
|
if (!getTid()) {
|
||||||
|
try {
|
||||||
|
const code = await navUniLoginCode()
|
||||||
|
const ready = await silentLogin(code, '')
|
||||||
|
if (!ready) return
|
||||||
|
await load(true)
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: '登录态刷新失败,请重试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不暂存卡片:避免企业申请页返回工作台时 onShow 再次自动跳回,形成循环
|
||||||
|
uni.navigateTo({ url: '/pages/onboarding/company' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if (goSubsystemByModule(card.code)) return
|
if (goSubsystemByModule(card.code)) return
|
||||||
if (card.items.length) {
|
const items = card.items || []
|
||||||
openMenu(card.items[0], card.name)
|
if (items.length) {
|
||||||
|
openMenu(items[0], card.name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (card.path) {
|
if (card.path) {
|
||||||
@@ -411,24 +599,20 @@ function enter(card) {
|
|||||||
uni.showToast({ title: card.name + '暂未开放', icon: 'none' })
|
uni.showToast({ title: card.name + '暂未开放', icon: 'none' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 引导登录:登录页登录成功后会回 nav(见 login.vue finishLogin) */
|
|
||||||
function goLogin(name = '') {
|
|
||||||
if (name) {
|
|
||||||
uni.showToast({ title: '「' + name + '」需登录后使用', icon: 'none' })
|
|
||||||
}
|
|
||||||
uni.navigateTo({ url: '/pages/login/login' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== 生命周期 ==================== */
|
/* ==================== 生命周期 ==================== */
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 默认落地页:未登录不再强制回登录页,而是以游客态渲染公开模块宫格
|
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
// 登录态可能在登录页/其他入口发生变化,每次展示重新判定并加载
|
// 注册 / 企业申请页回来后登录态可能变化,重新判定并加载
|
||||||
if (!loading.value) load()
|
if (!loading.value) load()
|
||||||
|
// 注册完成后自动继续进入用户刚才点击的模块
|
||||||
|
const pending = takePendingCard()
|
||||||
|
if (pending && isLoggedIn()) {
|
||||||
|
setTimeout(() => enter(pending), 600)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -627,26 +811,20 @@ onShow(() => {
|
|||||||
color: $color-text-muted;
|
color: $color-text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 游客态:卡片左上角标出免登录 / 需登录 */
|
/* 未授权时卡片是微信授权按钮:抹平 button 默认样式,保持与 view 卡片一致 */
|
||||||
.portal-card {
|
.cell-button {
|
||||||
position: relative;
|
display: block;
|
||||||
|
margin: 0 0 20rpx 0;
|
||||||
|
padding: 0 20rpx 0 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
line-height: normal;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
.portal-tag {
|
|
||||||
position: absolute;
|
|
||||||
top: 14rpx;
|
|
||||||
right: 14rpx;
|
|
||||||
padding: 2rpx 12rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
font-size: 18rpx;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.portal-tag.free {
|
|
||||||
color: #16a34a;
|
|
||||||
background: rgba(22, 163, 74, 0.12);
|
|
||||||
}
|
|
||||||
.portal-tag.need {
|
|
||||||
color: #94a3b8;
|
|
||||||
background: rgba(148, 163, 184, 0.16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-space {
|
.bottom-space {
|
||||||
@@ -692,4 +870,173 @@ onShow(() => {
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
padding: 16rpx;
|
padding: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ 企业选择抽屉(自绘,替代原生 actionSheet) ============ */
|
||||||
|
.tenant-picker-mask {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
background: rgba(15, 23, 42, .48);
|
||||||
|
@include mask-responsive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-picker-sheet {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
box-shadow: 0 -12rpx 40rpx rgba(15, 23, 42, .12);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
animation: tenant-sheet-in .22s ease-out;
|
||||||
|
@include sheet-responsive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tenant-sheet-in {
|
||||||
|
from { transform: translateY(60rpx); opacity: .6; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-grabber {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
margin: 16rpx auto 4rpx;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
background: #dbe3ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 20rpx $page-padding-x 24rpx;
|
||||||
|
border-bottom: 1rpx solid $color-divider;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-head-icon {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||||||
|
box-shadow: 0 8rpx 18rpx rgba(37, 99, 235, .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-head-copy { flex: 1; min-width: 0; }
|
||||||
|
.picker-title { display: block; font-size: 32rpx; font-weight: 700; letter-spacing: .5rpx; color: $color-text; }
|
||||||
|
.picker-sub { display: block; margin-top: 8rpx; font-size: 22rpx; line-height: 1.5; color: $color-text-muted; }
|
||||||
|
.picker-close {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
background: $color-bg-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 52vh;
|
||||||
|
padding: 24rpx $page-padding-x 4rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 24rpx 22rpx;
|
||||||
|
border: 1rpx solid #e6edf8;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(38, 86, 150, .05);
|
||||||
|
transition: transform .15s ease, border-color .15s ease, background .15s ease;
|
||||||
|
|
||||||
|
&:active { transform: scale(.985); background: #f7faff; border-color: #b9d2ff; }
|
||||||
|
&.current { border-color: #bfd4ff; background: #f5f9ff; }
|
||||||
|
&.disabled { opacity: .55; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-option-badge {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||||||
|
|
||||||
|
&.badge-1 { background: linear-gradient(135deg, #34d399, #0ea5a4); }
|
||||||
|
&.badge-2 { background: linear-gradient(135deg, #a78bfa, #6366f1); }
|
||||||
|
&.badge-3 { background: linear-gradient(135deg, #fbbf6e, #f97316); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-option-copy { flex: 1; min-width: 0; }
|
||||||
|
.tenant-option-name {
|
||||||
|
display: -webkit-box;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
.tenant-option-meta {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $color-text-muted;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tenant-current-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
background: $color-primary-bg;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
.tenant-option-loading { flex-shrink: 0; font-size: 22rpx; color: $color-primary; }
|
||||||
|
|
||||||
|
.picker-foot { padding: 8rpx $page-padding-x 20rpx; }
|
||||||
|
.picker-cancel {
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
background: #f1f5f9;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
|
||||||
|
&:active { background: #e6ecf5; }
|
||||||
|
}
|
||||||
|
.picker-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="hero">
|
||||||
|
<text class="eyebrow">YUNZE WORKSPACE</text>
|
||||||
|
<text class="title">加入企业</text>
|
||||||
|
<text class="desc">使用业务功能需要先创建企业,或申请挂靠已有企业</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 审核状态 -->
|
||||||
|
<view v-if="status" class="status-card" :class="statusClass">
|
||||||
|
<text class="status-title">{{ statusTitle }}</text>
|
||||||
|
<text class="status-desc">{{ statusDesc }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tabs">
|
||||||
|
<view class="tab" :class="{ active: tab === 'create' }" @tap="tab = 'create'">创建企业</view>
|
||||||
|
<view class="tab" :class="{ active: tab === 'join' }" @tap="tab = 'join'">挂靠企业</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 创建企业:沿用完整字段与后端校验 ============ -->
|
||||||
|
<view v-if="tab === 'create'" class="card">
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">企业全称 <text class="required">*</text></text>
|
||||||
|
<input v-model="createForm.tenant_name" class="input" placeholder="营业执照上的公司全称" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">统一社会信用代码 <text class="required">*</text></text>
|
||||||
|
<input v-model="createForm.credit_code" class="input" :maxlength="18" placeholder="18 位大写字母或数字" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">法定代表人 <text class="required">*</text></text>
|
||||||
|
<input v-model="createForm.legal_person" class="input" placeholder="姓名" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">营业执照 <text class="required">*</text></text>
|
||||||
|
<view class="upload" @tap="chooseLicense">
|
||||||
|
<text>{{ licenseName || '点击上传 JPG / PNG,不超过 5MB' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">联系人 <text class="required">*</text></text>
|
||||||
|
<input v-model="createForm.contact_person" class="input" placeholder="姓名" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">联系电话 <text class="required">*</text></text>
|
||||||
|
<input v-model="createForm.contact_phone" class="input" type="number" :maxlength="11" placeholder="手机号" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">电子邮箱 <text class="required">*</text></text>
|
||||||
|
<input v-model="createForm.contact_email" class="input" placeholder="用于接收审核结果" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">企业地址</text>
|
||||||
|
<input v-model="createForm.address" class="input" placeholder="选填" />
|
||||||
|
</view>
|
||||||
|
<text class="hint">提交后由平台管理员审核,通过后自动成为该企业管理员</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 挂靠企业 ============ -->
|
||||||
|
<view v-else class="card">
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">企业全称 <text class="required">*</text></text>
|
||||||
|
<view class="search-row">
|
||||||
|
<input v-model="keyword" class="input search-input" placeholder="输入企业全称后搜索" />
|
||||||
|
<view class="search-btn" @tap="searchTenants">搜索</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="tenantList.length" class="result-list">
|
||||||
|
<view v-for="t in tenantList" :key="t.tid" class="result-row" @tap="chooseTenant(t)">
|
||||||
|
<text>{{ t.tenant_name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="searched" class="no-result">未找到企业,请核对全称或改走「创建企业」</view>
|
||||||
|
<text v-if="selectedTenant" class="selected">已选:{{ selectedTenant.tenant_name }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">部门 <text class="required">*</text></text>
|
||||||
|
<picker :range="deptNames" :value="deptIndex" :disabled="!selectedTenant || !deptNames.length" @change="onDeptChange">
|
||||||
|
<view class="picker-value">{{ deptName() }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">职位 <text class="required">*</text></text>
|
||||||
|
<picker :range="positionNames" :value="positionIndex" :disabled="!positionNames.length" @change="onPositionChange">
|
||||||
|
<view class="picker-value">{{ positionName() }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">姓名 <text class="required">*</text></text>
|
||||||
|
<input v-model="applicantName" class="input" placeholder="真实姓名" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">手机号</text>
|
||||||
|
<view class="readonly"><text>{{ mobileMask || '微信已验证' }}</text><text class="readonly-tag">不可修改</text></view>
|
||||||
|
</view>
|
||||||
|
<text class="hint">提交后由该企业管理员审核,通过后即可使用业务功能</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="errorMsg" class="error"><text>{{ errorMsg }}</text></view>
|
||||||
|
<button class="submit" :disabled="loading || (status && status.has_tenant)" @tap="handleSubmit">
|
||||||
|
{{ loading ? '提交中…' : (tab === 'create' ? '提交创建申请' : '提交挂靠申请') }}
|
||||||
|
</button>
|
||||||
|
<view class="back" @tap="goBack"><text>返回工作台</text></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 需企业模块的引导页:创建企业(平台审核) / 挂靠企业(租户管理员审核)。
|
||||||
|
* 手机号由后端从微信验证结果读取,前端只读展示,不接受输入。
|
||||||
|
*/
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import {
|
||||||
|
getOnboardingStatus, searchOnboardingTenants, getOnboardingDepartments,
|
||||||
|
getOnboardingPositions, applyCreate, applyJoin
|
||||||
|
} from '@/api/onboarding.js'
|
||||||
|
import { getBaseURL } from '@/api/request.js'
|
||||||
|
|
||||||
|
const tab = ref('create')
|
||||||
|
const status = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMsg = ref('')
|
||||||
|
const mobileMask = ref('')
|
||||||
|
|
||||||
|
const createForm = reactive({
|
||||||
|
tenant_name: '', credit_code: '', legal_person: '', license_file: '',
|
||||||
|
contact_person: '', contact_phone: '', contact_email: '', address: ''
|
||||||
|
})
|
||||||
|
const licenseName = ref('')
|
||||||
|
|
||||||
|
const keyword = ref('')
|
||||||
|
const searched = ref(false)
|
||||||
|
const tenantList = ref([])
|
||||||
|
const selectedTenant = ref(null)
|
||||||
|
const deptList = ref([])
|
||||||
|
const positionList = ref([])
|
||||||
|
const deptIndex = ref(0)
|
||||||
|
const positionIndex = ref(0)
|
||||||
|
const applicantName = ref('')
|
||||||
|
|
||||||
|
const deptNames = computed(() => deptList.value.map((d) => d.name))
|
||||||
|
const positionNames = computed(() => positionList.value.map((p) => p.name))
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
if (!status.value) return ''
|
||||||
|
if (status.value.has_tenant) return 'passed'
|
||||||
|
const a = status.value.apply
|
||||||
|
if (!a) return ''
|
||||||
|
return a.status === 0 ? 'pending' : (a.status === 2 ? 'rejected' : 'passed')
|
||||||
|
})
|
||||||
|
const statusTitle = computed(() => {
|
||||||
|
if (!status.value) return ''
|
||||||
|
if (status.value.has_tenant) return '已加入 ' + (status.value.tenant_name || '企业')
|
||||||
|
const a = status.value.apply
|
||||||
|
if (!a) return '尚未提交申请'
|
||||||
|
if (a.status === 0) return '审核中'
|
||||||
|
if (a.status === 2) return '申请已驳回'
|
||||||
|
return '审核已通过'
|
||||||
|
})
|
||||||
|
const statusDesc = computed(() => {
|
||||||
|
if (!status.value) return ''
|
||||||
|
if (status.value.has_tenant) return '返回工作台即可使用业务功能'
|
||||||
|
const a = status.value.apply
|
||||||
|
if (!a) return '请选择「创建企业」或「挂靠企业」并提交资料'
|
||||||
|
if (a.status === 0) return '请等待管理员审核,通过后即可使用业务功能'
|
||||||
|
if (a.status === 2) return (a.reject_reason || '资料不符合要求') + ',可修改后重新提交'
|
||||||
|
return '审核已通过,返回工作台重新进入即可'
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => { loadStatus() })
|
||||||
|
|
||||||
|
async function loadStatus() {
|
||||||
|
try {
|
||||||
|
const res = await getOnboardingStatus()
|
||||||
|
status.value = res || null
|
||||||
|
mobileMask.value = (res && res.mobile_mask) || ''
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[onboarding] status failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseLicense() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const filePath = res.tempFilePaths && res.tempFilePaths[0]
|
||||||
|
if (!filePath) return
|
||||||
|
uni.showLoading({ title: '上传中...', mask: true })
|
||||||
|
uni.uploadFile({
|
||||||
|
url: getBaseURL().replace(/\/$/, '') + '/auth/register/upload-license',
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
success: (r) => {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(r.data || '{}')
|
||||||
|
if (body.code === 200 && body.data && body.data.url) {
|
||||||
|
createForm.license_file = body.data.url
|
||||||
|
licenseName.value = body.data.name || '已上传'
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: body.msg || '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => uni.showToast({ title: '上传失败', icon: 'none' }),
|
||||||
|
complete: () => uni.hideLoading()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchTenants() {
|
||||||
|
const kw = keyword.value.trim()
|
||||||
|
if (!kw) {
|
||||||
|
errorMsg.value = '请输入企业全称'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorMsg.value = ''
|
||||||
|
try {
|
||||||
|
const res = await searchOnboardingTenants(kw)
|
||||||
|
tenantList.value = (res && res.list) || []
|
||||||
|
searched.value = true
|
||||||
|
selectedTenant.value = null
|
||||||
|
deptList.value = []
|
||||||
|
positionList.value = []
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = (e && e.message) || '企业搜索失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseTenant(t) {
|
||||||
|
selectedTenant.value = t
|
||||||
|
keyword.value = t.tenant_name
|
||||||
|
deptIndex.value = 0
|
||||||
|
positionIndex.value = 0
|
||||||
|
positionList.value = []
|
||||||
|
try {
|
||||||
|
const res = await getOnboardingDepartments(t.tid)
|
||||||
|
deptList.value = (res && res.list) || []
|
||||||
|
if (deptList.value.length) await loadPositions()
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = (e && e.message) || '部门列表加载失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPositions() {
|
||||||
|
const dept = deptList.value[deptIndex.value]
|
||||||
|
if (!selectedTenant.value || !dept) return
|
||||||
|
try {
|
||||||
|
const res = await getOnboardingPositions(selectedTenant.value.tid, dept.id)
|
||||||
|
positionList.value = (res && res.list) || []
|
||||||
|
positionIndex.value = 0
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = (e && e.message) || '职位列表加载失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDeptChange(e) {
|
||||||
|
deptIndex.value = Number(e.detail.value) || 0
|
||||||
|
loadPositions()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPositionChange(e) {
|
||||||
|
positionIndex.value = Number(e.detail.value) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function deptName() {
|
||||||
|
const d = deptList.value[deptIndex.value]
|
||||||
|
return d ? d.name : (selectedTenant.value ? '请选择部门' : '请先搜索并选择企业')
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionName() {
|
||||||
|
const p = positionList.value[positionIndex.value]
|
||||||
|
return p ? p.name : (deptList.value.length ? '请选择职位' : '请先选择部门')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (loading.value) return
|
||||||
|
errorMsg.value = ''
|
||||||
|
if (status.value && status.value.has_tenant) {
|
||||||
|
goBack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (tab.value === 'create') await submitCreate()
|
||||||
|
else await submitJoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCreate() {
|
||||||
|
const f = createForm
|
||||||
|
f.tenant_name = f.tenant_name.trim()
|
||||||
|
f.credit_code = f.credit_code.trim().toUpperCase()
|
||||||
|
if (!f.tenant_name || !/^[0-9A-Z]{18}$/.test(f.credit_code) || !f.legal_person.trim() ||
|
||||||
|
!f.license_file || !f.contact_person.trim() || !/^1\d{10}$/.test(f.contact_phone.trim()) ||
|
||||||
|
!f.contact_email.includes('@')) {
|
||||||
|
errorMsg.value = '请完整填写企业信息:信用代码 18 位、营业执照、法人、联系人、11 位手机号与邮箱为必填'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await applyCreate({
|
||||||
|
tenant_name: f.tenant_name,
|
||||||
|
credit_code: f.credit_code,
|
||||||
|
legal_person: f.legal_person.trim(),
|
||||||
|
license_file: f.license_file,
|
||||||
|
contact_person: f.contact_person.trim(),
|
||||||
|
contact_phone: f.contact_phone.trim(),
|
||||||
|
contact_email: f.contact_email.trim(),
|
||||||
|
address: f.address.trim()
|
||||||
|
})
|
||||||
|
uni.showToast({ title: '已提交,等待平台审核', icon: 'none' })
|
||||||
|
await loadStatus()
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = (e && e.message) || '提交失败,请重试'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitJoin() {
|
||||||
|
if (!selectedTenant.value) {
|
||||||
|
errorMsg.value = '请先搜索并选择要挂靠的企业'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const dept = deptList.value[deptIndex.value]
|
||||||
|
const pos = positionList.value[positionIndex.value]
|
||||||
|
if (!dept || !pos || !applicantName.value.trim()) {
|
||||||
|
errorMsg.value = '请选择部门、职位并填写姓名'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await applyJoin({
|
||||||
|
tid: selectedTenant.value.tid,
|
||||||
|
applicant_name: applicantName.value.trim(),
|
||||||
|
department_id: dept.id,
|
||||||
|
department_name: dept.name,
|
||||||
|
position_id: pos.id,
|
||||||
|
position_name: pos.name
|
||||||
|
})
|
||||||
|
uni.showToast({ title: '已提交,等待企业管理员审核', icon: 'none' })
|
||||||
|
await loadStatus()
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = (e && e.message) || '提交失败,请重试'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/src/styles/page-common.scss';
|
||||||
|
|
||||||
|
.page { min-height: 100vh; padding: 0 $page-padding-x 60rpx; box-sizing: border-box; background: #f5f8fd; }
|
||||||
|
.hero { padding: 100rpx 8rpx 40rpx; }
|
||||||
|
.eyebrow { display: block; font-size: 20rpx; font-weight: 700; letter-spacing: 3rpx; color: #6e9de5; }
|
||||||
|
.title { display: block; margin-top: 14rpx; font-size: 44rpx; font-weight: 700; color: #17345f; }
|
||||||
|
.desc { display: block; margin-top: 14rpx; font-size: 25rpx; line-height: 1.6; color: #64748b; }
|
||||||
|
|
||||||
|
.status-card { @include card; padding: 26rpx 28rpx; margin-bottom: 28rpx; border-left: 8rpx solid #94a3b8; }
|
||||||
|
.status-card.pending { border-left-color: #f59e0b; }
|
||||||
|
.status-card.rejected { border-left-color: #ef4444; }
|
||||||
|
.status-card.passed { border-left-color: #10b981; }
|
||||||
|
.status-title { display: block; font-size: 30rpx; font-weight: 700; color: #0f172a; }
|
||||||
|
.status-desc { display: block; margin-top: 8rpx; font-size: 23rpx; line-height: 1.6; color: #64748b; }
|
||||||
|
|
||||||
|
.tabs { display: flex; gap: 12rpx; padding: 8rpx; margin-bottom: 24rpx; border-radius: $radius-full; background: #e8eef8; }
|
||||||
|
.tab { flex: 1; height: 76rpx; display: flex; align-items: center; justify-content: center; border-radius: $radius-full; font-size: 27rpx; color: #64748b; }
|
||||||
|
.tab.active { background: #ffffff; color: #2563eb; font-weight: 700; box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, .08); }
|
||||||
|
|
||||||
|
.card { @include card; padding: 32rpx 28rpx 36rpx; }
|
||||||
|
.field { margin-bottom: 26rpx; }
|
||||||
|
.label { display: block; margin-bottom: 12rpx; font-size: 25rpx; font-weight: 600; color: #334155; }
|
||||||
|
.required { color: #ef4444; }
|
||||||
|
.input { height: 84rpx; padding: 0 22rpx; border: 1rpx solid #dce7f7; border-radius: $radius-md; background: #fbfdff; font-size: 27rpx; color: $color-text; box-sizing: border-box; }
|
||||||
|
.hint { display: block; font-size: 22rpx; line-height: 1.6; color: $color-text-muted; }
|
||||||
|
.error { margin-top: 24rpx; font-size: 24rpx; color: $color-danger; }
|
||||||
|
|
||||||
|
.upload { min-height: 88rpx; padding: 24rpx 22rpx; border: 1rpx dashed #b9d2ff; border-radius: $radius-md; background: #f7faff; font-size: 24rpx; color: #2563eb; }
|
||||||
|
.search-row { display: flex; gap: 14rpx; }
|
||||||
|
.search-input { flex: 1; }
|
||||||
|
.search-btn { width: 140rpx; height: 84rpx; display: flex; align-items: center; justify-content: center; border-radius: $radius-md; background: #2563eb; color: #fff; font-size: 26rpx; }
|
||||||
|
.result-list { margin-top: 14rpx; border: 1rpx solid #e2e8f0; border-radius: $radius-md; overflow: hidden; }
|
||||||
|
.result-row { padding: 20rpx 22rpx; font-size: 26rpx; color: #334155; border-bottom: 1rpx solid #eef2f7; }
|
||||||
|
.result-row:last-child { border-bottom: none; }
|
||||||
|
.no-result { margin-top: 14rpx; font-size: 23rpx; color: #94a3b8; }
|
||||||
|
.selected { display: block; margin-top: 14rpx; font-size: 24rpx; color: #2563eb; }
|
||||||
|
.picker-value { height: 84rpx; line-height: 84rpx; padding: 0 22rpx; border: 1rpx solid #dce7f7; border-radius: $radius-md; background: #fbfdff; font-size: 27rpx; color: $color-text; }
|
||||||
|
.readonly { height: 84rpx; display: flex; align-items: center; justify-content: space-between; padding: 0 22rpx; border: 1rpx solid #e2e8f0; border-radius: $radius-md; background: #f1f5f9; font-size: 27rpx; color: #475569; }
|
||||||
|
.readonly-tag { font-size: 21rpx; color: #94a3b8; }
|
||||||
|
|
||||||
|
.submit { margin-top: 36rpx; height: 92rpx; line-height: 92rpx; border: none; border-radius: $radius-full; background: linear-gradient(135deg, #60a5fa, #2563eb); color: #fff; font-size: 30rpx; font-weight: 600; box-shadow: 0 10rpx 24rpx rgba(37, 99, 235, .22);
|
||||||
|
&::after { border: none; }
|
||||||
|
&[disabled] { opacity: .6; }
|
||||||
|
}
|
||||||
|
.back { margin-top: 28rpx; text-align: center; font-size: 25rpx; color: #94a3b8; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="hero">
|
||||||
|
<text class="eyebrow">YUNZE WORKSPACE</text>
|
||||||
|
<text class="title">设置登录密码</text>
|
||||||
|
<text class="desc">手机号 {{ mobileMask || '已验证' }} 尚未注册,设置密码后即可使用工作台</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">登录密码</text>
|
||||||
|
<input v-model="password" class="input" type="password" placeholder="至少 6 位" :maxlength="32" />
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">确认密码</text>
|
||||||
|
<input v-model="confirmPassword" class="input" type="password" placeholder="再输入一次" :maxlength="32" />
|
||||||
|
</view>
|
||||||
|
<text class="hint">密码至少 6 位;微信号会在注册时自动绑定,下次可直接进入</text>
|
||||||
|
<view v-if="errorMsg" class="error"><text>{{ errorMsg }}</text></view>
|
||||||
|
<button class="submit" :disabled="loading" @tap="handleSubmit">
|
||||||
|
{{ loading ? '注册中…' : '完成注册' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 手机号授权后未查到账号:设置密码完成注册(后端同时绑定微信 openid 并签发 tid=0 令牌)。
|
||||||
|
* 注册完成后回到工作台,由 nav 自动继续进入刚才点击的模块。
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { onboardingRegister } from '@/api/onboarding.js'
|
||||||
|
import { setAuthTokens, setTid, setTenantName } from '@/utils/auth.js'
|
||||||
|
import { guestMerge } from '@/api/guest.js'
|
||||||
|
import { getGuestToken, clearGuestToken } from '@/utils/guest.js'
|
||||||
|
import { clearHomeCache } from '@/api/workspace.js'
|
||||||
|
|
||||||
|
const token = ref('')
|
||||||
|
const mobileMask = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMsg = ref('')
|
||||||
|
|
||||||
|
onLoad((options = {}) => {
|
||||||
|
token.value = options.token || ''
|
||||||
|
mobileMask.value = decodeURIComponent(options.mobile_mask || '')
|
||||||
|
if (!token.value) {
|
||||||
|
errorMsg.value = '授权会话已失效,请返回工作台重新授权手机号'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (loading.value) return
|
||||||
|
errorMsg.value = ''
|
||||||
|
if (!token.value) {
|
||||||
|
errorMsg.value = '授权会话已失效,请返回工作台重新授权手机号'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.value.length < 6) {
|
||||||
|
errorMsg.value = '密码长度不能少于 6 位'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.value !== confirmPassword.value) {
|
||||||
|
errorMsg.value = '两次输入的密码不一致'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await onboardingRegister({
|
||||||
|
token: token.value,
|
||||||
|
password: password.value,
|
||||||
|
confirmPassword: confirmPassword.value
|
||||||
|
})
|
||||||
|
if (!res || !res.tokens || !res.tokens.access_token) throw new Error('注册失败,未返回登录态')
|
||||||
|
setAuthTokens(res.tokens)
|
||||||
|
setTid(0)
|
||||||
|
setTenantName('')
|
||||||
|
clearHomeCache()
|
||||||
|
// 注册前以游客身份产生的数据并入正式账号
|
||||||
|
if (getGuestToken()) {
|
||||||
|
try {
|
||||||
|
await guestMerge()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[onboarding] guest merge failed:', e)
|
||||||
|
}
|
||||||
|
clearGuestToken()
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '注册成功', icon: 'success' })
|
||||||
|
setTimeout(() => uni.reLaunch({ url: '/pages/nav/nav' }), 500)
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = (e && e.message) || '注册失败,请重试'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/src/styles/page-common.scss';
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0 $page-padding-x 60rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(160deg, #eaf3ff 0%, #f8fafc 42%, #f8fafc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero { padding: 120rpx 8rpx 56rpx; }
|
||||||
|
.eyebrow { display: block; font-size: 20rpx; font-weight: 700; letter-spacing: 3rpx; color: #6e9de5; }
|
||||||
|
.title { display: block; margin-top: 14rpx; font-size: 46rpx; font-weight: 700; color: #17345f; }
|
||||||
|
.desc { display: block; margin-top: 16rpx; font-size: 25rpx; line-height: 1.6; color: #64748b; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
@include card;
|
||||||
|
padding: 36rpx 30rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field { margin-bottom: 28rpx; }
|
||||||
|
.label { display: block; margin-bottom: 12rpx; font-size: 25rpx; font-weight: 600; color: #334155; }
|
||||||
|
.input {
|
||||||
|
height: 88rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
border: 1rpx solid #dce7f7;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
background: #fbfdff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $color-text;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.hint { display: block; font-size: 22rpx; line-height: 1.6; color: $color-text-muted; }
|
||||||
|
.error { margin-top: 20rpx; font-size: 24rpx; color: $color-danger; }
|
||||||
|
.submit {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
height: 92rpx;
|
||||||
|
line-height: 92rpx;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(37, 99, 235, .22);
|
||||||
|
|
||||||
|
&::after { border: none; }
|
||||||
|
&[disabled] { opacity: .6; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<text class="avatar-text">{{ avatarText }}</text>
|
<text class="avatar-text">{{ avatarText }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="profile-detail">
|
<view class="profile-detail">
|
||||||
<text class="profile-name">{{ user?.nickname || user?.name || '云泽用户' }}</text>
|
<text class="profile-name">{{ user?.nickname || user?.name || '温新云用户' }}</text>
|
||||||
<text class="profile-id">{{ user?.account ? `账号: ${user.account}` : `ID: ${userId}` }}</text>
|
<text class="profile-id">{{ user?.account ? `账号: ${user.account}` : `ID: ${userId}` }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="profile-edit" @tap="onEdit">
|
<view class="profile-edit" @tap="onEdit">
|
||||||
@@ -120,7 +120,7 @@ async function loadUser() {
|
|||||||
const merged = {
|
const merged = {
|
||||||
...prev,
|
...prev,
|
||||||
...data,
|
...data,
|
||||||
nickname: data.name || data.nickname || data.account || prev.nickname || '云泽用户'
|
nickname: data.name || data.nickname || data.account || prev.nickname || '温新云用户'
|
||||||
}
|
}
|
||||||
setUser(merged)
|
setUser(merged)
|
||||||
user.value = merged
|
user.value = merged
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// =============================================================
|
// =============================================================
|
||||||
// 云泽统一主题 · 冷调科技蓝(蓝白高级)
|
// 温新云统一主题 · 冷调科技蓝(蓝白高级)
|
||||||
// 品牌主色 #2563eb,浅蓝 #60a5fa,深蓝 #1d4ed8
|
// 品牌主色 #2563eb,浅蓝 #60a5fa,深蓝 #1d4ed8
|
||||||
// 背景冷灰白 #f8fafc,卡片纯白,柔和多层阴影,克制留白
|
// 背景冷灰白 #f8fafc,卡片纯白,柔和多层阴影,克制留白
|
||||||
// =============================================================
|
// =============================================================
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export function loginSuccess(payload = {}) {
|
|||||||
id: rawUser.id,
|
id: rawUser.id,
|
||||||
account: rawUser.account || '',
|
account: rawUser.account || '',
|
||||||
name: rawUser.name || '',
|
name: rawUser.name || '',
|
||||||
nickname: rawUser.nickname || rawUser.name || rawUser.account || '云泽用户',
|
nickname: rawUser.nickname || rawUser.name || rawUser.account || '温新云用户',
|
||||||
avatar: rawUser.avatar || '',
|
avatar: rawUser.avatar || '',
|
||||||
phone: rawUser.phone || payload.phone || '',
|
phone: rawUser.phone || payload.phone || '',
|
||||||
tid: rawUser.tid,
|
tid: rawUser.tid,
|
||||||
@@ -182,7 +182,7 @@ export function loginSuccess(payload = {}) {
|
|||||||
}
|
}
|
||||||
// 保证 nickname 可用
|
// 保证 nickname 可用
|
||||||
if (!user.nickname) {
|
if (!user.nickname) {
|
||||||
user.nickname = user.name || user.account || '云泽用户'
|
user.nickname = user.name || user.account || '温新云用户'
|
||||||
}
|
}
|
||||||
setUser(user)
|
setUser(user)
|
||||||
return user
|
return user
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 授权手机号 / 注册完成后,自动继续进入用户刚才点击的模块卡片。
|
||||||
|
* 只存最小字段,避免把响应式对象或函数写进 storage。
|
||||||
|
*/
|
||||||
|
const PENDING_KEY = 'app_pending_card'
|
||||||
|
|
||||||
|
export function setPendingCard(card) {
|
||||||
|
if (!card) {
|
||||||
|
uni.removeStorageSync(PENDING_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setStorageSync(PENDING_KEY, {
|
||||||
|
code: card.code || '',
|
||||||
|
name: card.name || '',
|
||||||
|
path: card.path || '',
|
||||||
|
needTenant: !!card.needTenant,
|
||||||
|
locked: !!card.locked,
|
||||||
|
items: (card.items || []).map((m) => ({
|
||||||
|
title: m.title || '',
|
||||||
|
path: m.path || '',
|
||||||
|
leaf: m.leaf !== false
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function takePendingCard() {
|
||||||
|
const card = uni.getStorageSync(PENDING_KEY) || null
|
||||||
|
uni.removeStorageSync(PENDING_KEY)
|
||||||
|
return card
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ export const sections: Record<SectionKey, SectionConfig> = {
|
|||||||
news: {
|
news: {
|
||||||
key: "news",
|
key: "news",
|
||||||
title: "新闻中心",
|
title: "新闻中心",
|
||||||
subtitle: "了解云泽科技的产品动态、公司公告与行业活动。",
|
subtitle: "了解温新云科技的产品动态、公司公告与行业活动。",
|
||||||
basePath: "/news",
|
basePath: "/news",
|
||||||
detailPath: "/news/detail",
|
detailPath: "/news/detail",
|
||||||
categoryPath: "/news/category",
|
categoryPath: "/news/category",
|
||||||
|
|||||||
@@ -27,17 +27,17 @@ export interface FooterColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const siteInfo = reactive({
|
export const siteInfo = reactive({
|
||||||
brandMark: "YZ",
|
brandMark: "WX",
|
||||||
brandName: "云泽科技",
|
brandName: "温新云科技",
|
||||||
brandSub: "YUNZER TECH",
|
brandSub: "WENXIN TECH",
|
||||||
fullName: "云泽科技有限公司",
|
fullName: "温新云科技有限公司",
|
||||||
slogan: "欢迎访问云泽科技官方网站",
|
slogan: "欢迎访问温新云科技官方网站",
|
||||||
phone: "400-000-0000",
|
phone: "400-000-0000",
|
||||||
email: "contact@yunzer.com",
|
email: "contact@yunzer.com",
|
||||||
address: "北京市海淀区中关村软件园 25 号楼",
|
address: "北京市海淀区中关村软件园 25 号楼",
|
||||||
postalCode: "100193",
|
postalCode: "100193",
|
||||||
copyright:
|
copyright:
|
||||||
"COPYRIGHT (C) 2026 YUNZER TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.",
|
"COPYRIGHT (C) 2026 WENXIN TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.",
|
||||||
|
|
||||||
// ↓↓↓ 以下字段由「官网管理 - 站点设置」下发(对应官网模板标签) ↓↓↓
|
// ↓↓↓ 以下字段由「官网管理 - 站点设置」下发(对应官网模板标签) ↓↓↓
|
||||||
/** {yz:sitename} 站点名称 */
|
/** {yz:sitename} 站点名称 */
|
||||||
|
|||||||
Reference in new issue
Block a user