diff --git a/.mimocode/login-payload.json b/.mimocode/login-payload.json index c910f3b..e69de29 100644 --- a/.mimocode/login-payload.json +++ b/.mimocode/login-payload.json @@ -1 +0,0 @@ -{"tenant_name":"连云港云泽广告传媒有限公司","account":"hero920103","password":"920103","lot_number":"test","pass_token":"test","gen_time":"test","captcha_output":"test"} diff --git a/backend/src/components/CommonHeader.vue b/backend/src/components/CommonHeader.vue index 68ab38b..1d74c37 100644 --- a/backend/src/components/CommonHeader.vue +++ b/backend/src/components/CommonHeader.vue @@ -7,7 +7,7 @@ - 云泽管理平台 + 温新云平台 {{ currentModule?.title || "数据赋能管理 · 科技创造价值" }} diff --git a/backend/src/views/apps/oa/schedule/index.vue b/backend/src/views/apps/oa/schedule/index.vue index 7fb0ee8..8bd016e 100644 --- a/backend/src/views/apps/oa/schedule/index.vue +++ b/backend/src/views/apps/oa/schedule/index.vue @@ -1378,7 +1378,13 @@ onMounted(() => { } .calendar-cell:hover { - background: #f5f8ff; + background: repeating-linear-gradient( + 135deg, + #fafcff 0, + #fafcff 6px, + #edf4ff 6px, + #edf4ff 12px + ); } .calendar-cell.other-month { @@ -1386,6 +1392,16 @@ onMounted(() => { background: #fafbfc; } +.calendar-cell.other-month:hover { + background: repeating-linear-gradient( + 135deg, + #fafbfc 0, + #fafbfc 6px, + #f2f4f8 6px, + #f2f4f8 12px + ); +} + .cell-date { display: flex; align-items: center; @@ -1435,7 +1451,13 @@ onMounted(() => { } .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; } +.calendar-cell.is-work:hover { + background: repeating-linear-gradient( + 135deg, + #ffffff 0, + #ffffff 6px, + #f2f5f9 6px, + #f2f5f9 12px + ); +} + /* 我的排班背景:上班绿 / 休息橙。排班是实际作息,覆盖节假日底色, 但保留休/班角标与节日名;today / selected 在后面定义,优先级仍最高。 仅靠提高底色饱和度区分,不加边框/竖条 */ @@ -1451,7 +1483,13 @@ onMounted(() => { } .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; 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); &: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 { color: var(--el-text-color-placeholder); background: var(--el-fill-color-light); - } - &.selected { - background: repeating-linear-gradient( - 135deg, - rgba(64, 158, 255, 0.14) 0, - rgba(64, 158, 255, 0.14) 6px, - rgba(64, 158, 255, 0.3) 6px, - rgba(64, 158, 255, 0.3) 12px - ); - box-shadow: inset 0 0 0 2px var(--el-color-primary); + + &:hover { + background: repeating-linear-gradient( + 135deg, + rgba(255, 255, 255, 0.02) 0, + rgba(255, 255, 255, 0.02) 6px, + rgba(255, 255, 255, 0.06) 6px, + rgba(255, 255, 255, 0.06) 12px + ); + } } } .month-tag { @@ -1865,24 +1919,80 @@ html.dark & { background: rgba(245, 108, 108, 0.07); &: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 { 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 { background: rgba(103, 194, 58, 0.1); &: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 { - 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 { - 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 + ); } } /* 今天:与浅色主题一致,只用绿色描边框,不覆盖排班底色 */ diff --git a/backend/src/views/basicSettings/applies/index.vue b/backend/src/views/basicSettings/applies/index.vue index d5e861e..a99f706 100644 --- a/backend/src/views/basicSettings/applies/index.vue +++ b/backend/src/views/basicSettings/applies/index.vue @@ -46,6 +46,15 @@
{{ scope.row.identity_mobile || "" }}
+ + + diff --git a/backend/src/views/settings/systeminfo/index.vue b/backend/src/views/settings/systeminfo/index.vue index e1de2cf..48d031e 100644 --- a/backend/src/views/settings/systeminfo/index.vue +++ b/backend/src/views/settings/systeminfo/index.vue @@ -12,7 +12,7 @@ - {{ systemInfo.name || '云泽管理系统' }} + {{ systemInfo.name || '温新云平台' }} {{ systemInfo.version || '1.0.0' }} @@ -227,7 +227,7 @@ async function fetchSystemInfo() { // 模拟数据(如果有API会替换) systemInfo.value = { - name: '云泽管理系统', + name: '温新云平台', version: '1.0.0', environment: 'Production', uptime: '24天 5小时 30分钟', diff --git a/go/controllers/app_aggregate.go b/go/controllers/app_aggregate.go index d54030a..d04ed81 100644 --- a/go/controllers/app_aggregate.go +++ b/go/controllers/app_aggregate.go @@ -97,6 +97,16 @@ func (c *AppAggregateController) Home() { return } 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)) 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。 // 供未登录用户渲染 nav 宫格,require_login 决定点击是「进登录引导」还是「直接进入」。 type publicModuleItem struct { diff --git a/go/controllers/auth/app_ticket.go b/go/controllers/auth/app_ticket.go index 7e97a2d..4f3c623 100644 --- a/go/controllers/auth/app_ticket.go +++ b/go/controllers/auth/app_ticket.go @@ -111,7 +111,7 @@ func (c *AuthAppTicketController) Exchange() { } var req struct { Ticket string `json:"ticket"` - Token string `json:"token"` // 兼容 mp-login 的命名 + Token string `json:"token"` // 兼容 mp-login 的命名 ClientID string `json:"client_id"` } if err := json.Unmarshal(body, &req); err != nil { @@ -143,7 +143,7 @@ func (c *AuthAppTicketController) Exchange() { return } - username := "云泽用户" + username := "温新云用户" if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" { username = strings.TrimSpace(*identity.Nickname) } diff --git a/go/controllers/auth/wechat_miniapp.go b/go/controllers/auth/wechat_miniapp.go index 4dc59c8..c265c82 100644 --- a/go/controllers/auth/wechat_miniapp.go +++ b/go/controllers/auth/wechat_miniapp.go @@ -17,7 +17,7 @@ import ( // 小程序登录失败原因(前端按此分支给出不同引导) const ( MiniReasonNotConfigured = "not_configured" // 服务端未配置小程序 - MiniReasonIdentityNotFound = "identity_not_found" // 微信未匹配到云泽账号 + MiniReasonIdentityNotFound = "identity_not_found" // 微信未匹配到温新云账号 MiniReasonNoTenant = "no_tenant" // 账号未加入任何企业 MiniReasonWechatAPI = "wechat_api_error" // 微信接口报错 ) @@ -120,7 +120,7 @@ func (c *AuthWechatMiniController) Login() { return } - // 定位云泽身份:手机号优先(已通过微信验证),其次查已绑定的小程序 openid + // 定位温新云身份:手机号优先(已通过微信验证),其次查已绑定的小程序 openid var mobile string if req.PhoneCode != "" { phone, perr := wechatmini.GetPhoneNumber(cfg, req.PhoneCode) @@ -137,7 +137,7 @@ func (c *AuthWechatMiniController) Login() { identity, err := c.resolveIdentity(cfg, mobile, session) if err != nil { - c.fail(MiniReasonIdentityNotFound, 404, "该微信未绑定云泽账号,请使用手机号一键登录或先注册") + c.fail(MiniReasonIdentityNotFound, 404, "该微信未绑定温新云账号,请使用手机号一键登录或先注册") return } if identity.Status != models.AuthIdentityStatusEnabled { @@ -147,15 +147,18 @@ func (c *AuthWechatMiniController) Login() { // 企业选择:单企业直接进入,多企业签发待选令牌由前端展示选择界面 tenants, lerr := authsvc.ListTenantOptions(identity.ID) - if lerr != nil || len(tenants) == 0 { - c.fail(MiniReasonNoTenant, 403, "该账号未加入任何企业,请联系管理员") + if lerr != nil { + c.fail("", 400, "获取企业列表失败,请重试") return } + // 新流程:账号已注册但尚未加入企业时仍签发正式令牌(tid=0), + // 端上可继续使用原免登录模块,并在进入需企业模块时引导创建/挂靠企业。 + noTenant := len(tenants) == 0 tid := uint64(0) needChoose := false if len(tenants) == 1 { tid = tenants[0].Tid - } else { + } else if len(tenants) > 1 { tid = authsvc.PendingTenantID needChoose = true } @@ -174,11 +177,12 @@ func (c *AuthWechatMiniController) Login() { } resp := map[string]interface{}{ - "code": 200, - "msg": "登录成功", - "tokens": tokens, - "tenants": tenants, - "identity": map[string]interface{}{"id": identity.ID, "nickname": username, "mobile": mobileOr(identity)}, + "code": 200, + "msg": "登录成功", + "tokens": tokens, + "tenants": tenants, + "identity": map[string]interface{}{"id": identity.ID, "nickname": username, "mobile": mobileOr(identity)}, + "need_tenant": noTenant, } if needChoose { resp["need_choose_tenant"] = true diff --git a/go/controllers/guest.go b/go/controllers/guest.go index 3121017..87428be 100644 --- a/go/controllers/guest.go +++ b/go/controllers/guest.go @@ -114,7 +114,7 @@ func (c *GuestController) MiniLogin() { return } - // 手机号已注册云泽账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。 + // 手机号已注册温新云账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。 // (与 /auth/wechat/miniapp/login 同一套识别与签发逻辑) if c.tryUserLogin(mobile, session) { return diff --git a/go/controllers/index_portal.go b/go/controllers/index_portal.go index ef5175e..d14b8ff 100644 --- a/go/controllers/index_portal.go +++ b/go/controllers/index_portal.go @@ -87,11 +87,11 @@ func (c *IndexPortalController) GetHeadMenu() { // 使用 OR 逻辑查询 name 或 short_name cond := orm.NewCondition() cond = cond.Or("tenant_name", tenantName).Or("tenant_short_name", tenantName) - + err := models.Orm.QueryTable(new(models.SystemTenant)). SetCond(cond). One(&tenant) - + if err != nil { fmt.Printf("[DEBUG] QueryTenant Error: %v, Looking for: %s\n", err, tenantName) } else { @@ -206,9 +206,9 @@ func (c *IndexPortalController) GetFooterData() { // 准备默认的 Footer 配置数据 footerMap := map[string]string{ - "sitename": "云泽官方网站", - "companyname": "云泽智能科技有限公司", - "copyright": "Copyright © 2026 Yunzer. All Rights Reserved.", + "sitename": "温新云官方网站", + "companyname": "连云港温新智能科技有限公司", + "copyright": "Copyright © 2026 WENXIN. All Rights Reserved.", "icp": "苏ICP备2026000000号-1", "description": "专业为企业建设数字化管理与官网门户的一站式 SaaS 服务平台", "logo": "", @@ -485,4 +485,4 @@ func (c *IndexPortalController) GetOnePageByPath() { "data": nil, } _ = c.ServeJSON() -} \ No newline at end of file +} diff --git a/go/controllers/onboarding.go b/go/controllers/onboarding.go new file mode 100644 index 0000000..5962e51 --- /dev/null +++ b/go/controllers/onboarding.go @@ -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 +} diff --git a/go/controllers/platform_website_setting.go b/go/controllers/platform_website_setting.go index a65c005..9e209e7 100644 --- a/go/controllers/platform_website_setting.go +++ b/go/controllers/platform_website_setting.go @@ -33,7 +33,6 @@ func (c *PlatformWebsiteSettingController) jsonErr(httpStatus, bizCode int, msg // Info GET /platform/website/setting/info // 直接把表里所有设置项返回,前端按行渲染(新增设置项无需改代码): // data.list = [{ id, title, value, tag, remark }] -// data.values = { "sitename": "云泽科技", ... }(key 即标签名,方便直接使用) func (c *PlatformWebsiteSettingController) Info() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) @@ -65,7 +64,6 @@ func (c *PlatformWebsiteSettingController) Info() { } // Save POST /platform/website/setting/save -// 入参 { "sitename": "云泽科技", "subtitle": "...", ... },key 为标签名,按标签逐项 upsert。 // 通用:表里已存在或新增的任意合法标签都可写入,不需要登记。 func (c *PlatformWebsiteSettingController) Save() { if _, err := requirePlatform(&c.Controller); err != nil { diff --git a/go/docs/sql/alter_auth_tenant_apply_onboarding.sql b/go/docs/sql/alter_auth_tenant_apply_onboarding.sql new file mode 100644 index 0000000..5e9a4f5 --- /dev/null +++ b/go/docs/sql/alter_auth_tenant_apply_onboarding.sql @@ -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`; diff --git a/go/docs/sql/create_platform_website_notice.sql b/go/docs/sql/create_platform_website_notice.sql index b6c4f6d..f2cc4cd 100644 --- a/go/docs/sql/create_platform_website_notice.sql +++ b/go/docs/sql/create_platform_website_notice.sql @@ -84,5 +84,5 @@ WHERE @notice_pid IS NOT NULL INSERT INTO `yz_platform_website_notice` (`title`, `summary`, `content`, `type`, `is_popup`, `top`, `sort`, `status`, `publish_time`) VALUES - ('云泽科技官网全新改版上线', '本次改版聚焦信息架构与访问体验,欢迎反馈建议。', - '

云泽科技官网全新改版上线,本次改版聚焦信息架构与访问体验。

', 1, 1, 1, 0, 1, NOW()); + ('温新云科技官网全新改版上线', '本次改版聚焦信息架构与访问体验,欢迎反馈建议。', + '

温新云科技官网全新改版上线,本次改版聚焦信息架构与访问体验。

', 1, 1, 1, 0, 1, NOW()); diff --git a/go/migrate_auth_conflict_20260919004551.csv b/go/migrate_auth_conflict_20260919004551.csv deleted file mode 100644 index 0e4bc1c..0000000 --- a/go/migrate_auth_conflict_20260919004551.csv +++ /dev/null @@ -1,2 +0,0 @@ -merge_key,row_count,password_variants,tids,representative_id -m:19895983967,2,2,1|2,4 diff --git a/go/migrate_auth_conflict_20260919004725.csv b/go/migrate_auth_conflict_20260919004725.csv deleted file mode 100644 index 0e4bc1c..0000000 --- a/go/migrate_auth_conflict_20260919004725.csv +++ /dev/null @@ -1,2 +0,0 @@ -merge_key,row_count,password_variants,tids,representative_id -m:19895983967,2,2,1|2,4 diff --git a/go/migrate_auth_map_20260919004727.csv b/go/migrate_auth_map_20260919004727.csv deleted file mode 100644 index 9e1fd56..0000000 --- a/go/migrate_auth_map_20260919004727.csv +++ /dev/null @@ -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 diff --git a/go/models/auth_tenant_apply.go b/go/models/auth_tenant_apply.go index 9525169..8dcd459 100644 --- a/go/models/auth_tenant_apply.go +++ b/go/models/auth_tenant_apply.go @@ -22,6 +22,13 @@ const ( type AuthTenantApply struct { ID uint64 `orm:"column(id);pk;auto" json:"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"` Tid uint64 `orm:"column(tid);default(0)" json:"tid"` TenantName string `orm:"column(tenant_name);size(128);default('')" json:"tenant_name"` diff --git a/go/output.log b/go/output.log deleted file mode 100644 index b475a1f..0000000 Binary files a/go/output.log and /dev/null differ diff --git a/go/routers/api/api.go b/go/routers/api/api.go index 35d9cc9..c165da9 100644 --- a/go/routers/api/api.go +++ b/go/routers/api/api.go @@ -185,4 +185,16 @@ func Register() { beego.Router("/api/guest/data/:id", &controllers.GuestController{}, "put:DataUpdate;delete:DataDelete") // 移动端用户自定义「常用功能」保存(读取随 /api/home 下发 quick_menu_ids) 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") } diff --git a/go/services/identity_register.go b/go/services/identity_register.go index e99cc4b..abc8cd7 100644 --- a/go/services/identity_register.go +++ b/go/services/identity_register.go @@ -436,8 +436,20 @@ func SearchTenantsForRegister(keyword string) ([]TenantSearchItem, error) { return out, nil } +// JoinApplyDetail 小程序「挂靠企业」补充资料。 +// 手机号不允许前端传:统一由后端从身份表读取微信验证过的手机号。 +type JoinApplyDetail struct { + ApplicantName string + DepartmentID uint64 + DepartmentName string + PositionID uint64 + PositionName string + WechatMobile string +} + // 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 { return nil, errors.New("参数错误") } @@ -477,7 +489,22 @@ func CreateJoinApply(identityID, tid uint64) (*models.AuthTenantApply, error) { TenantName: tenant.TenantName, Status: models.TenantApplyStatusPending, } - apply.ContactPerson = identityNickname(identity) + // 小程序挂靠表单:补充姓名 / 部门 / 职位,手机号取微信验证过的身份手机号 + 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) + } if identity.Mobile != nil { 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 { 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) } diff --git a/go/services/reminder_scheduler.go b/go/services/reminder_scheduler.go index 1ada761..ec21391 100644 --- a/go/services/reminder_scheduler.go +++ b/go/services/reminder_scheduler.go @@ -343,7 +343,7 @@ func (s *WechatMpSender) Send(ctx context.Context, reminder ReminderData, title, perr := wechatmp.PushToOpenID(cfg, openid, wechatmp.PushMessage{ Title: title, Content: content, - Remark: "来自云泽管理平台日程提醒", + Remark: "来自温新云平台日程提醒", }) if perr != nil { return false, perr diff --git a/go/services/wechatmini/api.go b/go/services/wechatmini/api.go index 51330db..c21547b 100644 --- a/go/services/wechatmini/api.go +++ b/go/services/wechatmini/api.go @@ -46,6 +46,8 @@ func (e *APIError) Explain() string { return "AppID 无效:请核对小程序后台「开发 → 开发管理」中的 AppID" case 40125: return "AppSecret 错误:请在小程序后台重置后复制新的 AppSecret 填入本页" + case 40164, 61004: + return "服务器出口 IP 不在白名单:请在小程序后台「开发管理 → 开发设置 → IP白名单」加入本服务器出口 IP" case 40226: return "该小程序未开通手机号快速验证能力:请在「开发管理 → 接口设置」中开通" case 45009: diff --git a/go/test-output-current.txt b/go/test-output-current.txt deleted file mode 100644 index c281cbd..0000000 Binary files a/go/test-output-current.txt and /dev/null differ diff --git a/go/test-output-tenant-login.txt b/go/test-output-tenant-login.txt deleted file mode 100644 index c281cbd..0000000 Binary files a/go/test-output-tenant-login.txt and /dev/null differ diff --git a/go/views/auth/forgot.tpl b/go/views/auth/forgot.tpl index b05367d..6f16643 100644 --- a/go/views/auth/forgot.tpl +++ b/go/views/auth/forgot.tpl @@ -187,7 +187,7 @@ - 云泽统一认证中心 + 温新云统一认证中心
找回登录密码
diff --git a/go/views/auth/login.tpl b/go/views/auth/login.tpl index d9dd6a7..ecd7d11 100644 --- a/go/views/auth/login.tpl +++ b/go/views/auth/login.tpl @@ -560,7 +560,7 @@ - 云泽统一认证中心 + 温新云统一认证中心 {{end}} @@ -592,7 +592,7 @@ {{end}} - 云泽统一认证中心 + 温新云统一认证中心 {{.LoginTitle}} @@ -769,7 +769,7 @@

微信扫码登录

-

使用微信「扫一扫」,扫码后在云泽小程序中点击确认
即可完成登录

+

使用微信「扫一扫」,扫码后在温新云小程序中点击确认
即可完成登录

二维码 5 分钟内有效,过期可点击刷新

@@ -799,7 +799,7 @@
- © 2026 云泽科技 版权所有 + © 2026 云泽 版权所有
diff --git a/platform/src/views/settings/systeminfo/index.vue b/platform/src/views/settings/systeminfo/index.vue index f970468..977e495 100644 --- a/platform/src/views/settings/systeminfo/index.vue +++ b/platform/src/views/settings/systeminfo/index.vue @@ -12,7 +12,7 @@ - {{ systemInfo.name || '云泽管理系统' }} + {{ systemInfo.name || '温新云平台' }} {{ systemInfo.version || '1.0.0' }} @@ -226,7 +226,7 @@ async function fetchSystemInfo() { // 模拟数据(如果有API会替换) systemInfo.value = { - name: '云泽管理系统', + name: '温新云平台', version: '1.0.0', environment: 'Production', uptime: '24天 5小时 30分钟', diff --git a/platform/src/views/system/aisettings/playground.vue b/platform/src/views/system/aisettings/playground.vue index 9047b15..5e80be3 100644 --- a/platform/src/views/system/aisettings/playground.vue +++ b/platform/src/views/system/aisettings/playground.vue @@ -216,7 +216,7 @@ const totalTokens = computed(() => const scenes = [ { label: "通用问答", prompt: "用三句话介绍一下你自己" }, { label: "代码生成", prompt: "用 Vue3 写一个防抖函数示例" }, - { label: "文案翻译", prompt: "把「欢迎使用云泽科技」翻译成英文和日文" }, + { label: "文案翻译", prompt: "把「欢迎使用温新云科技」翻译成英文和日文" }, { label: "OCR 提示词", prompt: "请把图片中的文字完整识别出来,保持原有排版" }, ]; diff --git a/platform/src/views/system/platformsettings/components/loginBrandSettings.vue b/platform/src/views/system/platformsettings/components/loginBrandSettings.vue index 061d953..96536d9 100644 --- a/platform/src/views/system/platformsettings/components/loginBrandSettings.vue +++ b/platform/src/views/system/platformsettings/components/loginBrandSettings.vue @@ -98,7 +98,7 @@ class="preview-logo" alt="Logo" /> -
云泽统一认证中心
+
温新云统一认证中心

{{ form.login_title || "一次登录,全平台通行" }}

{{ form.login_subtitle || "基于 OIDC / OAuth 2.0 的统一身份底座" }} diff --git a/platform/src/views/system/platformsettings/components/loginSettings.vue b/platform/src/views/system/platformsettings/components/loginSettings.vue index 83d392c..74ee9f1 100644 --- a/platform/src/views/system/platformsettings/components/loginSettings.vue +++ b/platform/src/views/system/platformsettings/components/loginSettings.vue @@ -23,7 +23,7 @@ -

微信扫码登录(云泽小程序内确认)。关闭后登录页不再显示该 tab。
+
微信扫码登录(温新云小程序内确认)。关闭后登录页不再显示该 tab。
- + diff --git a/platform/src/views/tenantApply/index.vue b/platform/src/views/tenantApply/index.vue index 7640a4c..8ccd2c8 100644 --- a/platform/src/views/tenantApply/index.vue +++ b/platform/src/views/tenantApply/index.vue @@ -68,6 +68,15 @@
{{ scope.row.identity_mobile || "" }}
+ + + {{ avatarText }} - {{ user.name || '云泽用户' }} - {{ user.tenant || '云泽平台' }} · {{ user.account }} + {{ user.name || '温新云用户' }} + {{ user.tenant || '温新云平台' }} · {{ user.account }} @@ -32,8 +32,8 @@ {{ avatarText }} - {{ user.name || '云泽用户' }} - {{ user.tenant || '云泽平台' }} · {{ user.account }} + {{ user.name || '温新云用户' }} + {{ user.tenant || '温新云平台' }} · {{ user.account }} diff --git a/uniapp/manifest.json b/uniapp/manifest.json index 7f52e44..d09e241 100644 --- a/uniapp/manifest.json +++ b/uniapp/manifest.json @@ -81,7 +81,7 @@ }, "vueVersion" : "3", "h5" : { - "title" : "云泽系统", + "title" : "温新云科技", "router" : { "mode" : "hash", "base" : "/h5/" diff --git a/uniapp/pages.json b/uniapp/pages.json index f82c6c3..a96137a 100644 --- a/uniapp/pages.json +++ b/uniapp/pages.json @@ -50,6 +50,20 @@ "navigationBarTitleText": "确认注册" } }, + { + "path": "pages/onboarding/register", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "设置密码" + } + }, + { + "path": "pages/onboarding/company", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "加入企业" + } + }, { "path": "pages/login/geetest-webview", "style": { @@ -621,7 +635,7 @@ ], "globalStyle": { "navigationBarTextStyle": "black", - "navigationBarTitleText": "云泽", + "navigationBarTitleText": "温新云", "navigationBarBackgroundColor": "#F8FAFC", "backgroundColor": "#F8FAFC", "rpxCalcMaxDeviceWidth": 560, diff --git a/uniapp/pages/dashboard/dashboard.vue b/uniapp/pages/dashboard/dashboard.vue index 44014a4..598366d 100644 --- a/uniapp/pages/dashboard/dashboard.vue +++ b/uniapp/pages/dashboard/dashboard.vue @@ -221,7 +221,7 @@ const orgLine = computed(() => { if (user.value.tenant) parts.push(user.value.tenant) if (user.value.account) parts.push(user.value.account) 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 || ''))) diff --git a/uniapp/pages/login/login.vue b/uniapp/pages/login/login.vue index b8a7614..14b31f9 100644 --- a/uniapp/pages/login/login.vue +++ b/uniapp/pages/login/login.vue @@ -20,7 +20,7 @@ - 云泽统一认证 + 温新云统一认证 一次登录,全平台通行 diff --git a/uniapp/pages/nav/nav.vue b/uniapp/pages/nav/nav.vue index 8937650..f9eaff9 100644 --- a/uniapp/pages/nav/nav.vue +++ b/uniapp/pages/nav/nav.vue @@ -4,18 +4,18 @@ - YUNZE WORKSPACE + WENXIN WORKSPACE 工作台 - + - 当前租户{{ user.tenant || '云泽平台' }} + 当前企业{{ user.tenant || '尚未加入企业' }} 切换 - {{ guest ? '免登录可逛部分功能,登录后使用完整工作台' : '选择一个业务系统,开始高效工作' }} + @@ -36,8 +36,28 @@ + + + + + {{ card.name }} - {{ card.needLogin ? '需登录' : '免登录' }} @@ -58,11 +77,58 @@ - + + + + + + + + + + {{ tenantPickerMode === 'login' ? '选择要进入的企业' : '切换企业' }} + {{ tenantPickerMode === 'login' ? '该账号已加入多家企业,请选择工作空间' : '选择一家企业,工作台将随之刷新' }} + + + + + + + + + + + + + {{ item.tenant_name || '企业' + item.tid }} + {{ item.name ? item.name + ' · ' : '' }}企业 ID {{ item.tid }} + + 当前 + 进入中… + + + + + + + + 选择企业后才能继续使用工作台 + + 取消 + + + @@ -73,8 +139,10 @@ import { mpHeadPad } from '@/utils/mpHead.js' const headPad = mpHeadPad() import { onShow } from '@dcloudio/uni-app' import { isLoggedIn, getTenants, getTid, setAuthTokens, setTenants, setTid, setTenantName, getUser, setUser } from '@/utils/auth.js' -import { switchTenant } from '@/api/auth.js' -import { fetchHome, fetchPublicModules, fetchPublicMenus } from '@/api/workspace.js' +import { switchTenant, loginByMiniapp } from '@/api/auth.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 { getSubsystemByModule, goSubsystem } from '@/config/subsystems.js' import { getGuestToken, setGuestToken, clearGuestToken } from '@/utils/guest.js' @@ -86,6 +154,12 @@ const guestAuthVisible = ref(false) const guestAuthLoading = ref(false) // 本次停留内跳过:关闭后不在 nav 反复弹窗,下次冷启动再引导 const guestAuthDismissed = ref(false) +const phoneBusy = ref(false) +// 非微信端没有 getPhoneNumber 能力:卡片退化为普通点击并给出提示 +let isMp = false +// #ifdef MP-WEIXIN +isMp = true +// #endif /** 仅微信小程序可拿手机号动态凭证 */ function canRequestGuestPhone() { @@ -170,36 +244,83 @@ async function completeUserLogin(res) { 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) { - uni.showActionSheet({ - itemList: list.map((t) => t.tenant_name || `企业${t.tid}`), - success: async ({ tapIndex }) => { - const tenant = list[tapIndex] - if (!tenant) return - uni.showLoading({ title: '登录中...', mask: true }) - try { - const sw = await switchTenant({ tid: tenant.tid }) - if (!sw || !sw.tokens) throw new Error((sw && sw.msg) || '进入企业失败') - setAuthTokens(sw.tokens) - setTenants(list) - setTid(tenant.tid) - setTenantName(tenant.tenant_name || '') - // 先游客后转正:选定企业后移植游客数据 - await mergeGuestIfAny() - await load(true) - uni.showToast({ title: '已登录,欢迎回来', icon: 'none' }) - } catch (e) { - uni.showToast({ title: (e && e.message) || '进入失败,请重试', icon: 'none' }) - } finally { - uni.hideLoading() - } - }, - fail: () => { - uni.showToast({ title: '请选择要进入的企业', icon: 'none' }) - setTimeout(() => askChooseTenant(list), 800) + openTenantPicker(list, 'login', true) +} + +async function pickTenant(item) { + if (!item || isPickingAny() || isCurrentTenant(item)) return + const isSwitch = tenantPickerMode.value === 'switch' + tenantPickingTid.value = String(item.tid) + switchingTenant.value = true + uni.showLoading({ title: isSwitch ? '正在切换...' : '登录中...', mask: true }) + try { + const res = await switchTenant({ tid: item.tid }) + if (!res || !res.tokens) throw new Error((res && res.msg) || (isSwitch ? '切换企业失败' : '进入企业失败')) + setAuthTokens(res.tokens) + setTenants(tenantPickerList.value) + setTid(item.tid) + 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 load(true) + uni.showToast({ title: isSwitch ? '已切换企业' : '已登录,欢迎回来', icon: isSwitch ? 'success' : 'none' }) + } catch (e) { + console.error('[nav] switch tenant failed:', e) + uni.showToast({ title: (e && e.message) || '进入失败,请重试', icon: 'none' }) + } finally { + uni.hideLoading() + tenantPickingTid.value = '' + switchingTenant.value = false + } } function skipGuestAuth() { @@ -262,17 +383,17 @@ const cards = computed(() => { if (guest.value) { return publicModules.value.map((mod, index) => { const code = String(mod.code || '').toLowerCase() - const needLogin = !guestMenuModuleSet.value.has(code) + const needTenant = mod.require_login !== 0 return { id: mod.id, code: mod.code, path: mod.path || '', name: mod.name || '未命名系统', - desc: mod.description || (needLogin ? '需登录使用' : '免登录'), + desc: mod.description || (needTenant ? '加入企业后可用' : '可直接使用'), icon: MODULE_ICONS[code] || 'cube', tint: 'tint-' + (index % 8), locked: false, - needLogin, + needTenant, items: [], total: 0 } @@ -303,6 +424,7 @@ const cards = computed(() => { icon: MODULE_ICONS[code] || 'cube', tint: locked ? 'tint-off' : 'tint-' + (index % 8), locked, + needTenant: mod.require_login !== 0, items, total: items.length } @@ -329,8 +451,7 @@ async function load(force = false) { } finally { loading.value = false } - // 进小程序即引导授权:游客无令牌时弹手机号授权窗 - maybeAskGuestAuth() + // 新流程:不再自动弹授权,点任意模块卡片时才拉起微信手机号授权 return } try { @@ -352,56 +473,123 @@ async function load(force = false) { function chooseTenant() { 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 } - if (switchingTenant.value) return - uni.showActionSheet({ - 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 }) - try { - const res = await switchTenant({ tid: tenant.tid }) - if (!res || !res.tokens) throw new Error((res && res.msg) || '切换租户失败') - setAuthTokens(res.tokens) - setTid(tenant.tid) - setTenantName(tenant.tenant_name || '') - const profile = getUser() || {} - const nextUser = { ...profile, tid: tenant.tid, tenant_name: tenant.tenant_name || '' } - setUser(nextUser) - user.value = { ...user.value, tenant: tenant.tenant_name || '', account: nextUser.account || user.value.account } - await load(true) - uni.showToast({ title: '已切换租户', icon: 'success' }) - } catch (e) { - uni.showToast({ title: (e && e.message) || '切换失败,请重试', icon: 'none' }) - } finally { - uni.hideLoading() - switchingTenant.value = false - } + if (switchingTenant.value || tenantPickerVisible.value) return + openTenantPicker(list, 'switch', false) +} + +/* ==================== 手机号授权 / 注册 / 企业分流 ==================== */ + +/** 点卡片(未授权时)直接触发微信手机号授权;有账号静默登录,无账号去设密码注册 */ +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 { + const code = await navUniLoginCode() + let appid = '' + try { + appid = (uni.getAccountInfoSync().miniProgram || {}).appId || '' + } catch (err) { + appid = '' } - }) + setPendingCard(card) + 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 enter(card) + takePendingCard() + } catch (err) { + console.error('[nav] onboarding phone failed:', err) + uni.showToast({ title: (err && err.message) || '授权失败,请重试', icon: 'none' }) + } finally { + uni.hideLoading() + 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)。 - * 未在 config/subsystems.js 注册的模块,降级为直接打开 Web 端该模块首页。 - * 游客:命中「必须登录」模块时不直接进,先引导登录(登录后回 nav)。 + * 需企业模块:查审核状态,未通过则引导创建 / 挂靠企业;通过后刷新企业令牌再进入。 */ -function enter(card) { +async function enter(card) { if (card.locked) { openMenu({ locked: true, title: card.name }) return } - if (guest.value && card.needLogin) { - goLogin(card.name) + if (guest.value) { + uni.showToast({ title: '请先授权手机号', icon: 'none' }) 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 (card.items.length) { - openMenu(card.items[0], card.name) + const items = card.items || [] + if (items.length) { + openMenu(items[0], card.name) return } if (card.path) { @@ -411,24 +599,20 @@ function enter(card) { 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(() => { - // 默认落地页:未登录不再强制回登录页,而是以游客态渲染公开模块宫格 load() }) onShow(() => { - // 登录态可能在登录页/其他入口发生变化,每次展示重新判定并加载 + // 注册 / 企业申请页回来后登录态可能变化,重新判定并加载 if (!loading.value) load() + // 注册完成后自动继续进入用户刚才点击的模块 + const pending = takePendingCard() + if (pending && isLoggedIn()) { + setTimeout(() => enter(pending), 600) + } }) @@ -627,26 +811,20 @@ onShow(() => { color: $color-text-muted; } -/* 游客态:卡片左上角标出免登录 / 需登录 */ -.portal-card { - position: relative; -} -.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); +/* 未授权时卡片是微信授权按钮:抹平 button 默认样式,保持与 view 卡片一致 */ +.cell-button { + 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; + } } .bottom-space { @@ -692,4 +870,173 @@ onShow(() => { color: #94a3b8; 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; +} diff --git a/uniapp/pages/onboarding/company.vue b/uniapp/pages/onboarding/company.vue new file mode 100644 index 0000000..e0bc8d7 --- /dev/null +++ b/uniapp/pages/onboarding/company.vue @@ -0,0 +1,402 @@ + + + + + diff --git a/uniapp/pages/onboarding/register.vue b/uniapp/pages/onboarding/register.vue new file mode 100644 index 0000000..0cf4a1e --- /dev/null +++ b/uniapp/pages/onboarding/register.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/uniapp/pages/profile/profile.vue b/uniapp/pages/profile/profile.vue index 5b7735f..78eddb7 100644 --- a/uniapp/pages/profile/profile.vue +++ b/uniapp/pages/profile/profile.vue @@ -6,7 +6,7 @@ {{ avatarText }} - {{ user?.nickname || user?.name || '云泽用户' }} + {{ user?.nickname || user?.name || '温新云用户' }} {{ user?.account ? `账号: ${user.account}` : `ID: ${userId}` }} @@ -120,7 +120,7 @@ async function loadUser() { const merged = { ...prev, ...data, - nickname: data.name || data.nickname || data.account || prev.nickname || '云泽用户' + nickname: data.name || data.nickname || data.account || prev.nickname || '温新云用户' } setUser(merged) user.value = merged diff --git a/uniapp/src/styles/theme.scss b/uniapp/src/styles/theme.scss index e892136..a40d5bf 100644 --- a/uniapp/src/styles/theme.scss +++ b/uniapp/src/styles/theme.scss @@ -1,5 +1,5 @@ // ============================================================= -// 云泽统一主题 · 冷调科技蓝(蓝白高级) +// 温新云统一主题 · 冷调科技蓝(蓝白高级) // 品牌主色 #2563eb,浅蓝 #60a5fa,深蓝 #1d4ed8 // 背景冷灰白 #f8fafc,卡片纯白,柔和多层阴影,克制留白 // ============================================================= diff --git a/uniapp/utils/auth.js b/uniapp/utils/auth.js index 9e41a71..495a0c0 100644 --- a/uniapp/utils/auth.js +++ b/uniapp/utils/auth.js @@ -172,7 +172,7 @@ export function loginSuccess(payload = {}) { id: rawUser.id, account: rawUser.account || '', name: rawUser.name || '', - nickname: rawUser.nickname || rawUser.name || rawUser.account || '云泽用户', + nickname: rawUser.nickname || rawUser.name || rawUser.account || '温新云用户', avatar: rawUser.avatar || '', phone: rawUser.phone || payload.phone || '', tid: rawUser.tid, @@ -182,7 +182,7 @@ export function loginSuccess(payload = {}) { } // 保证 nickname 可用 if (!user.nickname) { - user.nickname = user.name || user.account || '云泽用户' + user.nickname = user.name || user.account || '温新云用户' } setUser(user) return user diff --git a/uniapp/utils/onboarding.js b/uniapp/utils/onboarding.js new file mode 100644 index 0000000..ad604ad --- /dev/null +++ b/uniapp/utils/onboarding.js @@ -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 +} diff --git a/website/src/config/sections.ts b/website/src/config/sections.ts index f310836..92a8187 100644 --- a/website/src/config/sections.ts +++ b/website/src/config/sections.ts @@ -23,7 +23,7 @@ export const sections: Record = { news: { key: "news", title: "新闻中心", - subtitle: "了解云泽科技的产品动态、公司公告与行业活动。", + subtitle: "了解温新云科技的产品动态、公司公告与行业活动。", basePath: "/news", detailPath: "/news/detail", categoryPath: "/news/category", diff --git a/website/src/config/site.ts b/website/src/config/site.ts index 9c84403..355c1b0 100644 --- a/website/src/config/site.ts +++ b/website/src/config/site.ts @@ -27,17 +27,17 @@ export interface FooterColumn { } export const siteInfo = reactive({ - brandMark: "YZ", - brandName: "云泽科技", - brandSub: "YUNZER TECH", - fullName: "云泽科技有限公司", - slogan: "欢迎访问云泽科技官方网站", + brandMark: "WX", + brandName: "温新云科技", + brandSub: "WENXIN TECH", + fullName: "温新云科技有限公司", + slogan: "欢迎访问温新云科技官方网站", phone: "400-000-0000", email: "contact@yunzer.com", address: "北京市海淀区中关村软件园 25 号楼", postalCode: "100193", copyright: - "COPYRIGHT (C) 2026 YUNZER TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.", + "COPYRIGHT (C) 2026 WENXIN TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.", // ↓↓↓ 以下字段由「官网管理 - 站点设置」下发(对应官网模板标签) ↓↓↓ /** {yz:sitename} 站点名称 */