小程序提交

This commit is contained in:
Ghost
2026-01-07 11:00:09 +08:00
parent c15bce23a4
commit 0e79aca04e
54 changed files with 7324 additions and 75 deletions

801
pages/channel/add.vue Normal file
View File

@@ -0,0 +1,801 @@
<template>
<view class="page-container">
<!-- 验证中提示 -->
<view v-if="validating" class="validating-container">
<view class="validating-content">
<uni-icons type="spinner-cycle" size="40" color="#6366f1"></uni-icons>
<text class="validating-text">正在验证...</text>
</view>
</view>
<!-- 错误提示页面 -->
<view v-if="!validating && !tokenValid && errorMessage" class="error-container">
<view class="error-content">
<view class="error-icon-wrapper">
<uni-icons type="closeempty" size="80" color="#ef4444"></uni-icons>
</view>
<text class="error-title">验证失败</text>
<text class="error-message">{{ errorMessage }}</text>
<button class="error-button" @click="goBack">返回</button>
</view>
</view>
<!-- 创建方式选择 -->
<view class="method-selector" v-if="tokenValid && !validating && !errorMessage">
<view class="selector-item" :class="{'active': createMethod === 'manual'}" @click="createMethod = 'manual'">
<view class="selector-dot" v-if="createMethod === 'manual'"></view>
<text class="selector-text">手动创建</text>
</view>
</view>
<!-- 表单内容 -->
<view class="form-container" v-if="tokenValid && !validating && !errorMessage">
<view class="form-header">
<text class="form-title">填写渠道信息</text>
<text class="form-subtitle">请完善下方信息以手动添加新的分销渠道</text>
</view>
<view class="form-body">
<!-- 渠道名称 -->
<view class="form-item">
<view class="form-label">
<uni-icons type="shop" size="18" color="#6366f1"></uni-icons>
<text class="label-text">渠道名称</text>
<text class="required-mark">必填</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="form.channelName"
placeholder="请输入渠道名称"
maxlength="50"
:focus="focusChannelName"
@blur="focusChannelName = false"
/>
</view>
</view>
<!-- 联系电话 -->
<view class="form-item">
<view class="form-label">
<uni-icons type="phone" size="18" color="#6366f1"></uni-icons>
<text class="label-text">手机号</text>
<text class="required-mark">必填</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="form.contactPhone"
placeholder="请输入手机号"
type="number"
maxlength="11"
:focus="focusContactPhone"
@blur="focusContactPhone = false"
/>
</view>
</view>
<!-- 微信号 -->
<view class="form-item">
<view class="form-label">
<uni-icons type="chat" size="18" color="#6366f1"></uni-icons>
<text class="label-text">微信号</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="form.wechatId"
placeholder="请输入微信号"
maxlength="50"
:focus="focusWechatId"
@blur="focusWechatId = false"
/>
</view>
</view>
<!-- 备注 -->
<view class="form-item">
<view class="form-label">
<uni-icons type="info" size="18" color="#6366f1"></uni-icons>
<text class="label-text">备注</text>
</view>
<view class="input-wrapper">
<textarea
class="form-textarea"
v-model="form.remark"
placeholder="请输入备注信息 (选填)"
maxlength="200"
:auto-height="true"
:focus="focusRemark"
@blur="focusRemark = false"
/>
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="button-container" v-if="tokenValid && !validating && !errorMessage">
<button class="cancel-button" @click="cancel">取消</button>
<button class="submit-button" :class="{'loading': isSubmitting}" @click="submit" :disabled="isSubmitting || !tokenValid">
<uni-icons v-if="!isSubmitting" type="checkmarkempty" size="20" color="#fff"></uni-icons>
<text class="button-text">{{ isSubmitting ? '提交中...' : '创建' }}</text>
</button>
</view>
</view>
</template>
<script>
import config from '@/config';
export default {
data() {
return {
token: '',
createMethod: 'manual',
form: {
channelName: '',
contactPhone: '',
wechatId: '',
remark: ''
},
focusChannelName: false,
focusContactPhone: false,
focusWechatId: false,
focusRemark: false,
isSubmitting: false,
tokenValid: false,
validating: true,
errorMessage: ''
}
},
onLoad(options) {
// 获取参数
let scene = options.scene;
let token = options.token;
console.log('页面参数:', { scene, token });
// 小程序端解析 scene 参数
// #ifdef MP-WEIXIN
if (scene != undefined) {
// 先对 scene 进行 URL 解码(处理 %2C 等编码字符)
try {
scene = decodeURIComponent(scene);
} catch (e) {
console.warn('scene 解码失败,使用原始值:', e);
}
// scene 中可能包含 token或者直接就是 token
// 如果 scene 包含逗号,可能需要分割(根据实际业务需求调整)
if (scene.includes(',')) {
// 如果 scene 包含逗号,取第一部分作为 token
const parts = scene.split(',');
this.token = parts[0] || '';
} else {
// 如果不包含逗号scene 本身就是 token
this.token = scene;
}
} else if (token != undefined) {
// 兼容直接传 token 参数
this.token = token;
} else {
// 没有参数,显示错误
this.token = '';
}
// #endif
// H5 端兼容处理
// #ifdef H5
if (token != undefined) {
// H5 端优先使用 token 参数
this.token = token;
} else if (scene != undefined) {
// H5 也可能有 scene 参数(从分享链接等)
// 先对 scene 进行 URL 解码
try {
scene = decodeURIComponent(scene);
} catch (e) {
console.warn('scene 解码失败,使用原始值:', e);
}
if (scene.includes(',')) {
const parts = scene.split(',');
this.token = parts[0] || '';
} else {
this.token = scene;
}
} else {
// 没有参数,显示错误
this.token = '';
}
// #endif
console.log('解析结果:', { token: this.token });
if (!this.token) {
this.validating = false;
this.tokenValid = false;
this.errorMessage = '缺少token参数请重新扫码';
return;
}
uni.setNavigationBarTitle({
title: '新增渠道'
});
// 验证token有效性
this.validateToken();
// H5和小程序兼容处理
// #ifdef H5
// H5端回车键提交
document.onkeydown = (event) => {
const e = event || window.event;
if (e && e.keyCode === 13) {
this.submit();
}
};
// #endif
},
methods: {
// 验证token有效性
validateToken() {
this.validating = true;
uni.showLoading({
title: '验证中...',
mask: true
});
uni.request({
method: 'GET',
url: config.apiUrl2 + '/v1/frontend/distribution/channel/register',
data: {
token: this.token
},
header: {
'content-type': 'application/x-www-form-urlencoded'
},
success: (response) => {
uni.hideLoading();
this.validating = false;
if (response.data.code === 10000 || response.data.code === 200) {
this.tokenValid = true;
this.errorMessage = '';
// token验证成功可以填写表单
} else {
this.tokenValid = false;
this.errorMessage = response.data.message || 'token无效或已过期请重新扫码';
}
},
fail: (error) => {
uni.hideLoading();
this.validating = false;
this.tokenValid = false;
console.error('验证token失败:', error);
this.errorMessage = '网络错误,请检查网络连接后重试';
}
});
},
// 表单验证
validateForm() {
if (!this.form.channelName || !this.form.channelName.trim()) {
this.focusChannelName = true;
uni.showToast({
title: '请输入渠道名称',
icon: 'none',
duration: 2000
});
return false;
}
// 手机号必填验证
if (!this.form.contactPhone || !this.form.contactPhone.trim()) {
this.focusContactPhone = true;
uni.showToast({
title: '请输入手机号',
icon: 'none',
duration: 2000
});
return false;
}
// 验证手机号格式
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(this.form.contactPhone.trim())) {
this.focusContactPhone = true;
uni.showToast({
title: '请输入正确的手机号',
icon: 'none',
duration: 2000
});
return false;
}
return true;
},
// 提交表单
submit() {
if (this.isSubmitting) {
return;
}
if (!this.validateForm()) {
return;
}
if (!this.tokenValid) {
uni.showToast({
title: 'token无效请重新扫码',
icon: 'none',
duration: 2000
});
return;
}
this.isSubmitting = true;
const data = {
token: this.token,
name: this.form.channelName.trim(),
phone: this.form.contactPhone.trim(),
wechatId: this.form.wechatId ? this.form.wechatId.trim() : '',
remarks: this.form.remark ? this.form.remark.trim() : ''
};
uni.request({
method: 'POST',
url: config.apiUrl2 + '/v1/frontend/distribution/channel/register',
data: data,
header: {
'content-type': 'application/x-www-form-urlencoded'
},
success: (response) => {
this.isSubmitting = false;
if (response.data.code === 10000 || response.data.code === 200) {
// 获取返回的companyId
const responseData = response.data.data || {};
const companyId = responseData.companyId || '';
console.log('渠道创建成功,返回数据:', responseData);
console.log('获取到的companyId:', companyId);
uni.showToast({
title: '提交成功',
icon: 'success',
duration: 2000
});
setTimeout(() => {
// 如果有companyId跳转到渠道登录页
if (companyId) {
uni.redirectTo({
url: `/pages/channel/login?companyId=${companyId}`
});
} else {
// 如果没有companyId返回上一页
uni.navigateBack();
}
}, 1500);
} else {
// 显示后端返回的错误信息
const errorMsg = response.data.message || response.data.msg || response.data.error || '提交失败,请重试';
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
});
}
},
fail: (error) => {
this.isSubmitting = false;
console.error('提交失败:', error);
uni.showToast({
title: '网络错误,请重试',
icon: 'none',
duration: 2000
});
}
});
},
// 取消
cancel() {
uni.navigateBack();
},
// 返回
goBack() {
uni.navigateBack();
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 160rpx;
}
/* 创建方式选择器 */
.method-selector {
padding: 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.selector-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #f9fafb;
border-radius: 12rpx;
position: relative;
}
.selector-item.active {
background-color: #f0f4ff;
}
.selector-dot {
width: 24rpx;
height: 24rpx;
background-color: #6366f1;
border-radius: 50%;
margin-right: 16rpx;
}
.selector-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
/* 表单容器 */
.form-container {
background-color: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 40rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.form-header {
margin-bottom: 40rpx;
}
.form-title {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.form-subtitle {
display: block;
font-size: 26rpx;
color: #999;
line-height: 1.6;
}
.form-item {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
}
.form-label {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.label-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-left: 8rpx;
}
.required-mark {
font-size: 24rpx;
color: #ef4444;
margin-left: auto;
}
.input-wrapper {
position: relative;
}
.form-input {
width: 100%;
height: 88rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
background-color: #f9fafb;
border: 2rpx solid #e5e7eb;
border-radius: 12rpx;
box-sizing: border-box;
transition: all 0.3s ease;
/* #ifdef H5 */
&:focus {
outline: none;
border-color: #6366f1;
background-color: #fff;
box-shadow: 0 0 0 4rpx rgba(99, 102, 241, 0.1);
}
/* #endif */
/* #ifdef MP-WEIXIN */
&:focus {
border-color: #6366f1;
background-color: #fff;
}
/* #endif */
}
.form-input::placeholder {
color: #999;
}
.form-textarea {
width: 100%;
min-height: 160rpx;
padding: 24rpx;
font-size: 28rpx;
color: #333;
background-color: #f9fafb;
border: 2rpx solid #e5e7eb;
border-radius: 12rpx;
box-sizing: border-box;
line-height: 1.6;
transition: all 0.3s ease;
/* #ifdef H5 */
&:focus {
outline: none;
border-color: #6366f1;
background-color: #fff;
box-shadow: 0 0 0 4rpx rgba(99, 102, 241, 0.1);
}
/* #endif */
/* #ifdef MP-WEIXIN */
&:focus {
border-color: #6366f1;
background-color: #fff;
}
/* #endif */
}
.form-textarea::placeholder {
color: #999;
}
/* 底部按钮容器 */
.button-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx;
background-color: #fff;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 20rpx;
/* #ifdef H5 */
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
/* #endif */
z-index: 100;
}
.cancel-button {
flex: 1;
height: 88rpx;
background-color: #f5f5f5;
border: none;
border-radius: 12rpx;
color: #666;
font-size: 32rpx;
font-weight: 500;
/* #ifdef H5 */
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background-color: #e5e5e5;
}
/* #endif */
}
.submit-button {
flex: 2;
height: 88rpx;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border: none;
border-radius: 12rpx;
color: #fff;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.3);
/* #ifdef H5 */
cursor: pointer;
transition: all 0.3s ease;
&:hover:not(:disabled) {
transform: translateY(-2rpx);
box-shadow: 0 12rpx 32rpx rgba(99, 102, 241, 0.4);
}
&:active:not(:disabled) {
transform: translateY(0);
}
/* #endif */
}
.submit-button.loading {
opacity: 0.7;
pointer-events: none;
}
.submit-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.button-text {
margin-left: 8rpx;
}
/* H5适配 */
/* #ifdef H5 */
.page-container {
padding-bottom: 180rpx;
}
.button-container {
padding: 30rpx 40rpx;
}
/* #endif */
/* 小程序适配 */
/* #ifdef MP-WEIXIN */
.button-container {
padding-bottom: env(safe-area-inset-bottom);
}
/* #endif */
/* 验证中容器 */
.validating-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.validating-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.validating-text {
font-size: 28rpx;
color: #666;
}
/* 错误容器 */
.error-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 40rpx;
}
.error-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
max-width: 600rpx;
width: 100%;
}
.error-icon-wrapper {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #fef2f2;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
border: 4rpx solid #fee2e2;
}
.error-title {
font-size: 36rpx;
font-weight: 600;
color: #1f2937;
margin-bottom: 24rpx;
}
.error-message {
font-size: 28rpx;
color: #6b7280;
line-height: 1.6;
margin-bottom: 60rpx;
word-break: break-all;
}
.error-button {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border: none;
border-radius: 12rpx;
color: #fff;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.3);
/* #ifdef H5 */
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 12rpx 32rpx rgba(99, 102, 241, 0.4);
}
&:active {
transform: translateY(0);
}
/* #endif */
}
/* 响应式适配 */
@media (max-width: 375px) {
.form-container {
padding: 30rpx 20rpx;
}
.form-item {
margin-bottom: 32rpx;
}
}
</style>

319
pages/channel/list.vue Normal file
View File

@@ -0,0 +1,319 @@
<template>
<view class="page-container">
<!-- 列表内容 -->
<view class="list-container">
<u-list @scrolltolower="scrolltolower">
<u-list-item v-for="(item, index) in channelList" :key="index" class="list-item">
<view class="item-content" @click="editChannel(item)">
<view class="item-header">
<view class="item-title">{{ item.channelName || '未命名渠道' }}</view>
<view class="item-status" :class="{'status-active': item.status === 1}">
{{ item.status === 1 ? '启用' : '禁用' }}
</view>
</view>
<view class="item-info">
<view class="info-row" v-if="item.contactPhone">
<uni-icons type="phone" size="16" color="#999"></uni-icons>
<text class="info-text">{{ item.contactPhone }}</text>
</view>
<view class="info-row" v-if="item.wechatId">
<uni-icons type="chat" size="16" color="#999"></uni-icons>
<text class="info-text">{{ item.wechatId }}</text>
</view>
<view class="info-row" v-if="item.remark">
<uni-icons type="info" size="16" color="#999"></uni-icons>
<text class="info-text">{{ item.remark }}</text>
</view>
</view>
<view class="item-footer">
<text class="item-time">{{ formatTime(item.createTime) }}</text>
</view>
</view>
</u-list-item>
<u-loadmore :status="loadStatus" />
</u-list>
</view>
<!-- 底部添加按钮 -->
<view class="fab-container">
<button class="fab-button" @click="addChannel">
<uni-icons type="plus" size="24" color="#fff"></uni-icons>
<text class="fab-text">新增渠道</text>
</button>
</view>
</view>
</template>
<script>
import {
request
} from '@/utils/request';
export default {
data() {
return {
channelList: [],
page: 1,
pageSize: 20,
loadStatus: 'loadmore', // loadmore | loading | nomore
total: 0
}
},
onLoad() {
uni.setNavigationBarTitle({
title: '渠道管理'
});
this.loadChannelList();
},
onShow() {
// 从新增/编辑页返回时刷新列表
if (this.page === 1) {
this.loadChannelList();
}
},
onPullDownRefresh() {
// #ifdef MP-WEIXIN
this.page = 1;
this.channelList = [];
this.loadChannelList();
// #endif
},
methods: {
// 加载渠道列表
loadChannelList() {
this.loadStatus = 'loading';
request({
url: '/v1/frontend/business/channel/list',
data: {
page: this.page,
limit: this.pageSize
}
}).then((response) => {
// #ifdef MP-WEIXIN
uni.stopPullDownRefresh();
// #endif
if (response.data.code === 10000 || response.data.code === 200) {
const list = response.data.data.list || response.data.data.records || [];
if (this.page === 1) {
this.channelList = list;
} else {
this.channelList = [...this.channelList, ...list];
}
this.total = response.data.data.total || 0;
if (this.channelList.length >= this.total) {
this.loadStatus = 'nomore';
} else {
this.loadStatus = 'loadmore';
}
} else {
uni.showToast({
title: response.data.message || '加载失败',
icon: 'none',
duration: 2000
});
this.loadStatus = 'loadmore';
}
}).catch((error) => {
// #ifdef MP-WEIXIN
uni.stopPullDownRefresh();
// #endif
console.error('加载渠道列表失败:', error);
uni.showToast({
title: '网络错误,请重试',
icon: 'none',
duration: 2000
});
this.loadStatus = 'loadmore';
});
},
// 滚动到底部加载更多
scrolltolower() {
if (this.loadStatus === 'loadmore') {
this.page++;
this.loadChannelList();
}
},
// 新增渠道
addChannel() {
uni.navigateTo({
url: '/pages/channel/add'
});
},
// 编辑渠道
editChannel(item) {
uni.navigateTo({
url: `/pages/channel/add?id=${item.id || item.lId}`
});
},
// 格式化时间
formatTime(time) {
if (!time) return '';
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 100rpx;
}
.list-container {
padding: 20rpx;
}
.list-item {
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.item-content {
padding: 30rpx;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.item-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
flex: 1;
}
.item-status {
font-size: 24rpx;
color: #999;
padding: 8rpx 16rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
}
.status-active {
color: #22c55e;
background-color: #f0fdf4;
}
.item-info {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
}
.info-text {
font-size: 28rpx;
color: #666;
margin-left: 12rpx;
flex: 1;
}
.item-footer {
display: flex;
justify-content: flex-end;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
}
.item-time {
font-size: 24rpx;
color: #999;
}
/* 底部悬浮按钮 */
.fab-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx;
background-color: #fff;
border-top: 1rpx solid #f0f0f0;
/* #ifdef H5 */
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
/* #endif */
z-index: 100;
}
.fab-button {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border: none;
border-radius: 12rpx;
color: #fff;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.3);
/* #ifdef H5 */
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 12rpx 32rpx rgba(99, 102, 241, 0.4);
}
&:active {
transform: translateY(0);
}
/* #endif */
}
.fab-text {
margin-left: 12rpx;
}
/* H5适配 */
/* #ifdef H5 */
.page-container {
padding-bottom: 120rpx;
}
.fab-container {
padding: 30rpx 40rpx;
}
/* #endif */
/* 小程序适配 */
/* #ifdef MP-WEIXIN */
.fab-container {
padding-bottom: env(safe-area-inset-bottom);
}
/* #endif */
</style>

336
pages/channel/login.vue Normal file
View File

@@ -0,0 +1,336 @@
<template>
<view class="page-container">
<view class="hero">
<view class="hero-title">渠道登录</view>
<view class="hero-subtitle">登录后进入数据面板</view>
</view>
<!-- 账号密码登录H5 + 小程序通用 -->
<view class="card">
<view class="card-title">账号密码登录</view>
<uni-forms ref="h5Form" :modelValue="form">
<uni-forms-item name="phone">
<uni-easyinput
v-model="form.phone"
placeholder="请输入手机号"
type="number"
maxlength="11"
:inputBorder="false"
class="input"
/>
</uni-forms-item>
<uni-forms-item name="password">
<uni-easyinput
v-model="form.password"
placeholder="请输入密码"
:inputBorder="false"
type="password"
class="input"
/>
</uni-forms-item>
</uni-forms>
<view class="login-tip">
<text>默认初始密码为 </text>
<text class="login-tip-strong">123456</text>
<text>登录后请及时修改</text>
</view>
<button class="primary-btn" :disabled="isSubmitting" @click="handleH5Login">
<text>{{ isSubmitting ? '登录中...' : '登录' }}</text>
</button>
</view>
</view>
</template>
<script>
import config from '@/config'
export default {
data() {
return {
form: {
phone: '',
password: ''
},
isSubmitting: false,
companyId: ''
}
},
onLoad(options) {
// 获取参数
let scene = options.scene;
let companyId = options.companyId;
console.log('页面参数:', { scene, companyId });
// 小程序端解析 scene 参数
// #ifdef MP-WEIXIN
if (scene != undefined) {
// 先对 scene 进行 URL 解码(处理 %2C 等编码字符)
try {
scene = decodeURIComponent(scene);
} catch (e) {
console.warn('scene 解码失败,使用原始值:', e);
}
// scene 中可能包含 companyId或者直接就是 companyId
// 如果 scene 包含逗号,可能需要分割(根据实际业务需求调整)
if (scene.includes(',')) {
// 如果 scene 包含逗号,取第一部分作为 companyId
const parts = scene.split(',');
this.companyId = parts[0] || '';
} else {
// 如果不包含逗号scene 本身就是 companyId
this.companyId = scene;
}
} else if (companyId != undefined) {
// 兼容直接传 companyId 参数
this.companyId = companyId;
} else {
// 尝试从本地存储获取(之前可能保存过)
const storedCompanyId = uni.getStorageSync('channelCompanyId');
if (storedCompanyId) {
this.companyId = storedCompanyId;
console.log('从本地存储获取companyId:', storedCompanyId);
} else {
this.companyId = '';
}
}
// #endif
// H5 端兼容处理
// #ifdef H5
if (companyId != undefined) {
// H5 端优先使用 companyId 参数
this.companyId = companyId;
} else if (scene != undefined) {
// H5 也可能有 scene 参数(从分享链接等)
// 先对 scene 进行 URL 解码
try {
scene = decodeURIComponent(scene);
} catch (e) {
console.warn('scene 解码失败,使用原始值:', e);
}
if (scene.includes(',')) {
const parts = scene.split(',');
this.companyId = parts[0] || '';
} else {
this.companyId = scene;
}
} else {
// 尝试从本地存储获取
const storedCompanyId = uni.getStorageSync('channelCompanyId');
if (storedCompanyId) {
this.companyId = storedCompanyId;
console.log('从本地存储获取companyId:', storedCompanyId);
} else {
this.companyId = '';
}
}
// #endif
console.log('解析结果:', { companyId: this.companyId });
},
methods: {
// 账号密码登录H5 + 小程序)
handleH5Login() {
if (this.isSubmitting) return
if (!this.form.phone || !this.form.password) {
uni.showToast({ title: '请输入手机号和密码', icon: 'none', duration: 2000 })
return
}
if (!this.companyId) {
uni.showToast({ title: '缺少companyId参数', icon: 'none', duration: 2000 })
return
}
this.isSubmitting = true
// 使用和列表接口一样的请求方式和参数格式
uni.request({
method: 'POST',
url: config.apiUrl2 + '/v1/frontend/distribution/user/login',
data: {
companyId: this.companyId,
phone: this.form.phone,
password: this.form.password
},
header: {
'content-type': 'application/x-www-form-urlencoded'
},
success: (res) => {
this.isSubmitting = false
console.log('登录接口完整返回:', res)
console.log('登录接口返回数据:', res.data)
if (res.data.code === 10000 || res.data.code === 200) {
const data = res.data.data || {}
const token = data.token || ''
const channelInfo = data.channelInfo || {}
console.log('解析后的data:', data)
console.log('解析后的token:', token)
console.log('解析后的channelInfo:', channelInfo)
// 保存token
if (token) {
uni.setStorageSync('channelToken', token)
console.log('已保存token到本地存储')
} else {
console.error('token为空无法保存')
}
// 保存完整的登录数据(备用)
uni.setStorageSync('channelLoginData', JSON.stringify(data))
console.log('已保存完整登录数据到本地存储')
// 保存渠道信息 - 从channelInfo中获取
if (channelInfo.channelCode) {
uni.setStorageSync('channelCode', channelInfo.channelCode)
console.log('已保存channelCode:', channelInfo.channelCode)
} else {
console.warn('channelInfo.channelCode为空无法保存')
}
if (channelInfo.channelName) {
uni.setStorageSync('channelName', channelInfo.channelName)
console.log('已保存channelName:', channelInfo.channelName)
} else {
console.warn('channelInfo.channelName为空无法保存')
}
// 验证保存是否成功
const savedCode = uni.getStorageSync('channelCode')
const savedName = uni.getStorageSync('channelName')
const savedToken = uni.getStorageSync('channelToken')
console.log('验证保存结果:', { savedCode, savedName, savedToken })
if (!savedCode) {
console.error('警告channelCode保存失败')
uni.showToast({
title: '渠道编码保存失败,请重试',
icon: 'none',
duration: 3000
})
return
}
this.toStatistics()
} else {
const msg = res.data.message || res.data.msg || '登录失败'
console.error('登录失败:', msg)
uni.showToast({ title: msg, icon: 'none', duration: 3000 })
}
},
fail: (err) => {
console.error('登录失败', err)
this.isSubmitting = false
uni.showToast({ title: '网络错误,请重试', icon: 'none', duration: 2000 })
}
})
},
toStatistics() {
uni.redirectTo({
url: '/pages/channel/statistics'
})
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: linear-gradient(180deg, #f5f7ff 0%, #f8f8f8 100%);
padding: 40rpx 30rpx 80rpx;
box-sizing: border-box;
}
.hero {
margin-bottom: 30rpx;
}
.hero-title {
font-size: 44rpx;
font-weight: 700;
color: #1f2937;
}
.hero-subtitle {
margin-top: 8rpx;
font-size: 26rpx;
color: #6b7280;
}
.card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 28rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin-bottom: 24rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #111827;
margin-bottom: 24rpx;
}
.input {
background: #f5f6fa;
border-radius: 12rpx;
padding: 20rpx;
}
.login-tip {
margin-top: 16rpx;
margin-bottom: 8rpx;
font-size: 24rpx;
color: #6b7280;
line-height: 1.6;
}
.login-tip-strong {
font-weight: 600;
color: #ef4444;
}
.primary-btn {
width: 100%;
height: 90rpx;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border: none;
border-radius: 12rpx;
color: #fff;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
margin-top: 12rpx;
box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.25);
}
.tips {
margin-top: 16rpx;
font-size: 24rpx;
color: #6b7280;
text-align: center;
}
/* #ifdef H5 */
.primary-btn {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 10rpx 28rpx rgba(99, 102, 241, 0.3);
}
&:active {
transform: translateY(0);
}
}
/* #endif */
</style>

View File

@@ -0,0 +1,177 @@
<template>
<view class="page-container">
<view class="header">
<view class="title">选择项目</view>
<view class="subtitle">请选择要查看的渠道项目</view>
</view>
<view v-if="loading" class="loading">
<uni-icons type="spinner-cycle" size="32" color="#6366f1"></uni-icons>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="projects.length === 0" class="empty">
<uni-icons type="info" size="40" color="#ccc"></uni-icons>
<text class="empty-text">暂无可选项目</text>
</view>
<view v-else class="list">
<view
v-for="(item, index) in projects"
:key="index"
class="project-card"
@click="selectProject(item)"
>
<view class="project-main">
<view class="project-name">{{ item.name || '未命名项目' }}</view>
<view class="project-code">编码{{ item.code || '--' }}</view>
</view>
<uni-icons type="arrowright" size="20" color="#999"></uni-icons>
</view>
</view>
</view>
</template>
<script>
import config from '@/config'
export default {
data() {
return {
loading: false,
projects: []
}
},
onShow() {
this.fetchProjects()
},
methods: {
fetchProjects() {
const token = uni.getStorageSync('channelToken')
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none', duration: 2000 })
uni.redirectTo({ url: '/pages/channel/login' })
return
}
this.loading = true
uni.request({
method: 'GET',
url: config.apiUrl2 + '/v1/frontend/distribution/channel/projects',
header: {
'content-type': 'application/x-www-form-urlencoded',
token
},
success: (res) => {
this.loading = false
if (res.data.code === 10000 || res.data.code === 200) {
this.projects = res.data.data?.list || res.data.data || []
} else {
const msg = res.data.message || res.data.msg || '加载失败'
uni.showToast({ title: msg, icon: 'none', duration: 3000 })
}
},
fail: (err) => {
console.error('加载项目失败', err)
this.loading = false
uni.showToast({ title: '网络错误,请重试', icon: 'none', duration: 2000 })
}
})
},
selectProject(item) {
const name = encodeURIComponent(item.name || '')
const code = encodeURIComponent(item.code || '')
uni.navigateTo({
url: `/pages/channel/statistics?name=${name}&code=${code}`
})
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6fb;
padding: 32rpx 28rpx 60rpx;
box-sizing: border-box;
}
.header {
margin-bottom: 24rpx;
}
.title {
font-size: 38rpx;
font-weight: 700;
color: #111827;
}
.subtitle {
margin-top: 8rpx;
font-size: 26rpx;
color: #6b7280;
}
.loading,
.empty {
margin-top: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
color: #999;
}
.loading-text,
.empty-text {
font-size: 28rpx;
color: #666;
}
.list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-top: 8rpx;
}
.project-card {
background: #fff;
border-radius: 14rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.06);
}
.project-main {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.project-name {
font-size: 32rpx;
font-weight: 600;
color: #111827;
}
.project-code {
font-size: 26rpx;
color: #6b7280;
}
/* #ifdef H5 */
.project-card {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 8rpx 18rpx rgba(0, 0, 0, 0.08);
}
}
/* #endif */
</style>

2183
pages/channel/statistics.vue Normal file

File diff suppressed because it is too large Load Diff