chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// 这是系统自动生成的公共文件
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use think\captcha\facade\Captcha;
|
||||
use think\facade\Filesystem;
|
||||
use think\facade\Request;
|
||||
use ywxapp\controller\MemberBase;
|
||||
|
||||
/**
|
||||
* Ajax 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Ajax extends MemberBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['verify'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize(){}
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
public function verify()
|
||||
{
|
||||
ob_clean();
|
||||
return Captcha::create();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\Request;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use addon\articles\model\Article as ArticleModel;
|
||||
use addon\articles\model\ArticleCategory as CategoryModel;
|
||||
/**
|
||||
* Article 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Article extends MemberBase
|
||||
{
|
||||
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new ArticleModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index($page = 1, $limit = 15)
|
||||
{
|
||||
$param = $this->request->param();
|
||||
$user = $this->user();
|
||||
//$this->result->success($user,$user->uid);
|
||||
$query = ArticleModel::with(['category'])
|
||||
->where('uid', $user->uid)
|
||||
->order('create_at', 'desc');
|
||||
if (! empty($param['title'])) {
|
||||
$query->whereLike('title', "%{$param['title']}%");
|
||||
}
|
||||
|
||||
if (! empty($param['cid'])) {
|
||||
$query->where('cid', $param['cid']);
|
||||
}
|
||||
|
||||
if (isset($param['status'])) {
|
||||
$query->where('status', $param['status']);
|
||||
}
|
||||
|
||||
$list = $query->page((int) $page, (int) $limit)->select()->toArray();
|
||||
$count = $query->count();
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => 'success',
|
||||
'count' => $count,
|
||||
'data' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示创建资源表单页.
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
try {
|
||||
$user = app()->auth->info;
|
||||
$data = ArticleModel::create([
|
||||
'title' => '未命名草稿',
|
||||
'uid' => $user->uid,
|
||||
'content' => '',
|
||||
'status' => 0, // 草稿状态
|
||||
'create_at' => time(),
|
||||
'update_at' => time(),
|
||||
]);
|
||||
$cates = CategoryModel::select();
|
||||
$this->result->success([
|
||||
'category' => $cates,
|
||||
'info' => $data,
|
||||
], '草稿创建成功');
|
||||
} catch (\Exception $e) {
|
||||
// 记录日志,但不要因为入库失败而中断上传流程(或者根据需求决定)
|
||||
\think\facade\Log::error($e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save(Request $request)
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
$param = $this->request->param();
|
||||
Db::startTrans();
|
||||
try {
|
||||
$info = $this->model->findOrEmpty($param['id']);
|
||||
if ($info->isEmpty()) {
|
||||
$this->result->error("数据获取错误!");
|
||||
}
|
||||
$info->save($param);
|
||||
Db::commit();
|
||||
$this->result->success($info, '数据修改成功!');
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
Db::rollback();
|
||||
$this->result->error('数据修改失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function read($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑资源表单页.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$info = $this->model->findOrEmpty($id);
|
||||
if ($info->isEmpty()) {
|
||||
$this->result->error("数据获取错误!");
|
||||
}
|
||||
$cates = CategoryModel::select();
|
||||
$this->result->success([
|
||||
'category' => $cates,
|
||||
'info' => $info,
|
||||
], '数据创建成功');
|
||||
# code...
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
$param = $this->request->param();
|
||||
Db::startTrans();
|
||||
try {
|
||||
$info = $this->model->findOrEmpty($param['id']);
|
||||
if ($info->isEmpty()) {
|
||||
$this->result->error("数据获取错误!");
|
||||
}
|
||||
$info->save($param);
|
||||
Db::commit();
|
||||
$this->result->success($info, '数据修改成功!');
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
Db::rollback();
|
||||
$this->result->error('数据修改失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($ids, $force) {
|
||||
if ($force) {
|
||||
ArticleModel::onlyTrashed()->whereIn('id', $ids)->select()->each(function ($item) {
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
ArticleModel::destroy($ids);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $th) {
|
||||
$this->result->error('删除失败: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use ywxapp\model\Card as CardModel;
|
||||
use app\member\validate\Card as CardValidate;
|
||||
|
||||
/**
|
||||
* 会员卡密充值(充值中心 -> 卡密充值)
|
||||
*
|
||||
* 业务闭环:
|
||||
* 1) 会员在「卡密充值」页输入卡号 + 密码
|
||||
* 2) redeem() 校验:卡密存在 / status=0或1(未用)/ 未删除
|
||||
* 3) 事务内:钱包余额 += 面值、累计充值 += 面值;user_bill 记充值流水;卡密标记 status=2、use_time=now、绑定 uid
|
||||
* 4) 一次性用完即焚,重复兑换同一卡密会提示已使用
|
||||
*/
|
||||
class Card extends MemberBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
// 卡密表自愈(补齐 batch_no 等扩展列)
|
||||
\ywxapp\model\Card::ensureSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡密充值页
|
||||
*/
|
||||
public function exchange()
|
||||
{
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 兑换卡密(Ajax POST)
|
||||
*/
|
||||
public function redeem()
|
||||
{
|
||||
if (!($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('访问错误');
|
||||
}
|
||||
$user = $this->auth->model;
|
||||
if (empty($user)) {
|
||||
$this->result->error('请先登录', 401);
|
||||
}
|
||||
|
||||
$post = $this->request->only(['cardno', 'password'], 'post');
|
||||
$validate = new CardValidate();
|
||||
if (!$validate->scene('redeem')->check($post)) {
|
||||
$this->result->error($validate->getError());
|
||||
}
|
||||
|
||||
try {
|
||||
$result = CardModel::redeem((int)$user->uid, trim($post['cardno']), trim($post['password']));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[Card] redeem failed: ' . $e->getMessage());
|
||||
$this->result->error('兑换失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
$this->result->success($result['data'] ?? [], $result['msg']);
|
||||
}
|
||||
$this->result->error($result['msg']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的兑换记录(Ajax GET,会员中心内页)
|
||||
*/
|
||||
public function records()
|
||||
{
|
||||
$user = $this->auth->model;
|
||||
if (empty($user)) {
|
||||
$this->result->error('请先登录', 401);
|
||||
}
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$list = CardModel::where('uid', (int)$user->uid)
|
||||
->where('status', CardModel::STATUS_USED)
|
||||
->where('delete_at', 0)
|
||||
->order('use_time', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$items = $list->items();
|
||||
foreach ($items as &$row) {
|
||||
$row['use_time_text'] = $row['use_time'] > 0 ? date('Y-m-d H:i:s', $row['use_time']) : '';
|
||||
}
|
||||
unset($row);
|
||||
$this->result->setCount($list->total())->success($items);
|
||||
}
|
||||
return $this->view->fetch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use think\Request;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use ywxapp\controller\BaseController;
|
||||
use addon\articles\model\ArticleCategory as CategoryModel;
|
||||
|
||||
/**
|
||||
* Category 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Category extends MemberBase
|
||||
{
|
||||
/**
|
||||
* Summary of noNeedLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $needRight = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示创建资源表单页.
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function read($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑资源表单页.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
public function tabtree()
|
||||
{
|
||||
$data = CategoryModel::select();
|
||||
$tree = CategoryModel::tabTree($data);
|
||||
$this->result->success($tree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use ywxapp\controller\MemberBase;
|
||||
|
||||
/**
|
||||
* Index 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Index extends MemberBase
|
||||
{
|
||||
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize(){}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 会员中心主框架为完整 iframe 页面,不需要套用全局 layout(common/layout)
|
||||
$this->view->layout(false);
|
||||
$arrayVar = [
|
||||
"logo" => [
|
||||
"title" => "Wxapp Mmember",
|
||||
"src" => "static/images/logo.png",
|
||||
],
|
||||
"menu" => [
|
||||
"url" => "/user/index/menu",
|
||||
],
|
||||
"tabs" => [
|
||||
"home" => [
|
||||
"id" => "1",
|
||||
"href" => "console/index",
|
||||
"name" => "console:index",
|
||||
"title" => "首页",
|
||||
],
|
||||
],
|
||||
'container' => 'WXapp', // 容器 ID
|
||||
'entry' => 'index', // 默认视图文件名
|
||||
'route_prefix' => '/user/views/',
|
||||
'engine' => 'html', // 视图文件后缀名
|
||||
'name' => 'AdminUI', // 系统名称
|
||||
'theme' => [
|
||||
'color' => [
|
||||
['main' => '#20222A', 'selected' => '#16baaa', 'alias' => 'default'],
|
||||
['main' => '#03152A', 'selected' => '#3B91FF', 'alias' => 'dark-blue'],
|
||||
['main' => '#2E241B', 'selected' => '#A48566', 'alias' => 'coffee'],
|
||||
['main' => '#50314F', 'selected' => '#7A4D7B', 'alias' => 'purple-red'],
|
||||
['main' => '#344058', 'logo' => '#1E9FFF', 'selected' => '#1E9FFF', 'alias' => 'ocean'],
|
||||
['main' => '#3A3D49', 'logo' => '#2F9688', 'selected' => '#16b777', 'alias' => 'green'],
|
||||
['main' => '#20222A', 'logo' => '#F78400', 'selected' => '#F78400', 'alias' => 'red'],
|
||||
['main' => '#28333E', 'logo' => '#AA3130', 'selected' => '#AA3130', 'alias' => 'fashion-red'],
|
||||
['main' => '#24262F', 'logo' => '#3A3D49', 'selected' => '#16baaa', 'alias' => 'classic-black'],
|
||||
['logo' => '#226A62', 'header' => '#2F9688', 'alias' => 'green-header'],
|
||||
['main' => '#344058', 'logo' => '#0085E8', 'selected' => '#1E9FFF', 'header' => '#1E9FFF', 'alias' => 'ocean-header'],
|
||||
['header' => '#393D49', 'alias' => 'classic-black-header'],
|
||||
['main' => '#50314F', 'logo' => '#50314F', 'selected' => '#7A4D7B', 'header' => '#50314F', 'alias' => 'purple-red-header'],
|
||||
['main' => '#28333E', 'logo' => '#28333E', 'selected' => '#AA3130', 'header' => '#AA3130', 'alias' => 'fashion-red-header'],
|
||||
['main' => '#28333E', 'logo' => '#16baaa', 'selected' => '#16baaa', 'header' => '#16baaa', 'alias' => 'green-header'],
|
||||
['main' => '#393D49', 'logo' => '#393D49', 'selected' => '#16baaa', 'header' => '#23262E', 'alias' => 'Classic-style1'],
|
||||
['main' => '#001529', 'logo' => '#001529', 'selected' => '#1890FF', 'header' => '#1890FF', 'alias' => 'Classic-style2'],
|
||||
['main' => '#25282A', 'logo' => '#25282A', 'selected' => '#35BDB2', 'header' => '#35BDB2', 'alias' => 'Classic-style3'],
|
||||
],
|
||||
'initColorIndex' => 3, // 初始颜色索引
|
||||
],
|
||||
"other" => ["keepLoad" => "1200", "autoHead" => false, "footer" => false],
|
||||
];
|
||||
$this->view->assign($arrayVar);
|
||||
|
||||
// 注入当前登录用户,供右上角头像/昵称显示
|
||||
// 未登录时 $this->auth->model 为 null,需整体降级,避免 null->avatar 致命错误
|
||||
$authUser = $this->auth->model;
|
||||
$this->view->assign('member', [
|
||||
'nickname' => $authUser->nickname ?? '会员',
|
||||
'avatar' => $authUser->avatar ?? '/static/common/images/avatar.jpg',
|
||||
'account' => $authUser->account ?? '',
|
||||
]);
|
||||
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单数据
|
||||
*
|
||||
* 会员中心菜单:仅指向会员端真实可用页面(避免后台路由导致 403)。
|
||||
* 结构使用 child 键,供前端 buildSideMenu 递归渲染;route 为 iframe 内打开的地址。
|
||||
*/
|
||||
public function menu()
|
||||
{
|
||||
$tree = [
|
||||
[
|
||||
'name' => 'console',
|
||||
'title' => '控制台',
|
||||
'icon' => 'layui-icon-home',
|
||||
'route' => '/static/member/views/console/index.html',
|
||||
'child' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'task',
|
||||
'title' => '任务中心',
|
||||
'icon' => 'layui-icon-rate',
|
||||
'route' => '/user/task/index',
|
||||
'child' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'myprop',
|
||||
'title' => '我的道具',
|
||||
'icon' => 'layui-icon-gift',
|
||||
'route' => '/user/task/myProp',
|
||||
'child' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'mymedal',
|
||||
'title' => '我的勋章',
|
||||
'icon' => 'layui-icon-trophy',
|
||||
'route' => '/user/medal/index',
|
||||
'child' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'payment',
|
||||
'title' => '充值中心',
|
||||
'icon' => 'layui-icon-rmb',
|
||||
'route' => '#',
|
||||
'spread' => false,
|
||||
'child' => [
|
||||
[
|
||||
'name' => 'recharge',
|
||||
'title' => '套餐充值',
|
||||
'icon' => '',
|
||||
'route' => '/user/payment/index',
|
||||
'child' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'card',
|
||||
'title' => '卡密充值',
|
||||
'icon' => '',
|
||||
'route' => '/user/card/exchange',
|
||||
'child' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'cardrecord',
|
||||
'title' => '兑换记录',
|
||||
'icon' => '',
|
||||
'route' => '/user/card/records',
|
||||
'child' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'forum',
|
||||
'title' => '社区中心',
|
||||
'icon' => 'layui-icon-release',
|
||||
'route' => '#',
|
||||
'spread' => false,
|
||||
'child' => [
|
||||
[
|
||||
'name' => 'post',
|
||||
'title' => '我要发帖',
|
||||
'icon' => '',
|
||||
'route' => '/forum/member/forum/post',
|
||||
'child' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'mytopics',
|
||||
'title' => '我的帖子',
|
||||
'icon' => '',
|
||||
'route' => '/forum/member/forum/mytopics',
|
||||
'child' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->result->success($tree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use app\member\validate\Login as LoginValidate;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Event;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\controller\MemberBase;
|
||||
|
||||
/**
|
||||
* Login 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Login extends MemberBase
|
||||
{
|
||||
/**
|
||||
* 不需要登录验证的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* 不需要权限验证的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->view->layout(false);
|
||||
View::assign('title', '登录');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$data = $this->request->param();
|
||||
try {
|
||||
validate(LoginValidate::class)->check($data);
|
||||
$this->app->event->trigger('UserLoginBefore', $data);
|
||||
$this->auth->login($data['username'], $data['password'], true);
|
||||
if ($this->auth->isLogin) {
|
||||
return $this->result->success();
|
||||
}
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage(), 1);
|
||||
}
|
||||
}
|
||||
$this->result->error("请求访问错误!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$this->auth->logout();
|
||||
$this->result->success('退出登录成功');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use ywxapp\model\Medal as MedalModel;
|
||||
use ywxapp\controller\MemberBase;
|
||||
|
||||
/**
|
||||
* 会员勋章中心
|
||||
*/
|
||||
class Medal extends MemberBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
protected function initialize() {}
|
||||
|
||||
/**
|
||||
* 我的勋章页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return $this->view->fetch('medal/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的勋章列表(AJAX)
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
$uid = (int) ($this->auth->model->uid ?? 0);
|
||||
$medals = MedalModel::getUserMedals($uid);
|
||||
foreach ($medals as &$m) {
|
||||
$m['create_at_text'] = $m['create_at'] ? date('Y-m-d H:i', (int) $m['create_at']) : '';
|
||||
}
|
||||
unset($m);
|
||||
$this->result->success($medals);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use think\facade\Event;
|
||||
use think\Request;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use app\member\validate\Payment as PaymentValidate;
|
||||
|
||||
/**
|
||||
* 支付控制器
|
||||
*
|
||||
* 设计原则:核心只负责「订单/流程编排 + 事件触发」,具体支付方式由【支付插件】实现,
|
||||
* 通过监听以下事件注入能力(插件在 info.php 的 events 中声明监听器即可):
|
||||
*
|
||||
* - PaymentMethods 收集可用支付方式。监听器返回 array,如:
|
||||
* [['code'=>'wechat','name'=>'微信支付','desc'=>'...','icon'=>'','sort'=>0], ...]
|
||||
* - PaymentCreate 生成支付参数。监听器接收 ['order'=>[...],'params'=>[...]],
|
||||
* 返回网关参数 array(按需返回 null 表示不匹配该方法),如:
|
||||
* ['type'=>'qrcode','qrcode_url'=>'weixin://...','expire'=>600]
|
||||
* ['type'=>'redirect','redirect_url'=>'https://...']
|
||||
* ['type'=>'form','html'=>'<form ...>']
|
||||
* - PaymentNotify 异步回调。监听器接收 ['method'=>...,'request'=>[...],'get'=>[...]],
|
||||
* 完成验签与订单激活后,返回网关要求的响应字符串(如 'success')。
|
||||
* - PaymentFreeActive 免费套餐开通钩子(无支付)。
|
||||
* - PaymentOrderCreate 订单落库钩子(插件可在此持久化订单)。
|
||||
*
|
||||
* 未安装任何支付插件时,create() 会返回「暂无可用支付方式」提示。
|
||||
*/
|
||||
class Payment extends MemberBase
|
||||
{
|
||||
/**
|
||||
* 套餐配置(来自 config/member.php,价格单位:元,duration 单位:天)
|
||||
* @var array
|
||||
*/
|
||||
private $plans = [];
|
||||
|
||||
/**
|
||||
* 支付页 / 套餐列表(免登录可看)
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['index', 'methods'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->plans = config('member.plans', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付页面
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->view->assign('plans', array_values($this->plans));
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用支付方式(插件通过 PaymentMethods 事件注入)
|
||||
*/
|
||||
public function methods()
|
||||
{
|
||||
$results = Event::trigger('PaymentMethods', []);
|
||||
$methods = [];
|
||||
$codes = [];
|
||||
foreach ((array)$results as $r) {
|
||||
if (!is_array($r)) {
|
||||
continue;
|
||||
}
|
||||
foreach (array_values($r) as $m) {
|
||||
if (!is_array($m) || empty($m['code']) || in_array($m['code'], $codes, true)) {
|
||||
continue;
|
||||
}
|
||||
$codes[] = $m['code'];
|
||||
$methods[] = $m;
|
||||
}
|
||||
}
|
||||
// 按 sort 排序
|
||||
usort($methods, function ($a, $b) {
|
||||
return ($a['sort'] ?? 0) <=> ($b['sort'] ?? 0);
|
||||
});
|
||||
// 兜底:若没有任何插件注入支付方式(如插件系统未初始化),至少提供「个人免签」体验方式
|
||||
if (empty($methods)) {
|
||||
$methods[] = [
|
||||
'code' => 'personal',
|
||||
'name' => '个人免签',
|
||||
'desc' => '扫码转账到个人收款码,站长确认到账后开通',
|
||||
'icon' => '',
|
||||
'sort' => 0,
|
||||
];
|
||||
}
|
||||
$this->result->success($methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付订单
|
||||
* 1) 免费套餐:直接触发 PaymentFreeActive,无需支付
|
||||
* 2) 付费套餐:触发 PaymentOrderCreate(落库钩子)+ PaymentCreate(网关参数钩子)
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if (!($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('访问错误');
|
||||
}
|
||||
$user = $this->auth->model;
|
||||
$planId = (int)$this->request->param('plan_id', 0);
|
||||
$method = (string)$this->request->param('method', '');
|
||||
|
||||
$validate = new PaymentValidate();
|
||||
if (! $validate->check(['plan_id' => $planId, 'method' => $method])) {
|
||||
$this->result->error($validate->getError());
|
||||
}
|
||||
$plan = $this->getPlan($planId);
|
||||
if (!$plan) {
|
||||
$this->result->error('套餐不存在', 1);
|
||||
}
|
||||
|
||||
// 免费套餐直接开通
|
||||
if ($plan['price'] <= 0) {
|
||||
Event::trigger('PaymentFreeActive', ['member' => $user, 'plan' => $plan]);
|
||||
$this->result->success(['type' => 'free', 'plan' => $plan['title']]);
|
||||
}
|
||||
|
||||
if (!$method) {
|
||||
$this->result->error('请选择支付方式', 1);
|
||||
}
|
||||
|
||||
$order = [
|
||||
'order_sn' => $this->genOrderSn($user->uid),
|
||||
'uid' => $user->uid,
|
||||
'plan_id' => $planId,
|
||||
'plan_title' => $plan['title'],
|
||||
'amount' => $plan['price'],
|
||||
'method' => $method,
|
||||
'create_at' => time(),
|
||||
];
|
||||
|
||||
// 订单落库钩子(插件可持久化订单)
|
||||
Event::trigger('PaymentOrderCreate', ['order' => $order]);
|
||||
|
||||
// 生成支付参数:由各支付插件监听 PaymentCreate 返回网关数据
|
||||
$results = Event::trigger('PaymentCreate', [
|
||||
'order' => $order,
|
||||
'params' => $this->request->param(),
|
||||
]);
|
||||
$gateway = null;
|
||||
foreach ((array)$results as $r) {
|
||||
if (is_array($r) && !empty($r)) {
|
||||
$gateway = $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (empty($gateway)) {
|
||||
$this->result->error('暂无可用支付方式,请先安装支付插件', 1);
|
||||
}
|
||||
$this->result->success($gateway);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步回调(由各支付插件处理具体网关通知)
|
||||
* 插件监听 PaymentNotify 完成验签与订单激活,并返回网关要求的响应体。
|
||||
*/
|
||||
public function notify()
|
||||
{
|
||||
$method = (string)$this->request->param('method', '');
|
||||
$results = Event::trigger('PaymentNotify', [
|
||||
'method' => $method,
|
||||
'request' => $this->request->param(),
|
||||
'get' => $this->request->get(),
|
||||
'post' => $this->request->post(),
|
||||
]);
|
||||
foreach ((array)$results as $r) {
|
||||
if (is_string($r) && $r !== '') {
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据套餐 id 获取套餐配置
|
||||
*/
|
||||
private function getPlan(int $id): ?array
|
||||
{
|
||||
return $this->plans[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
private function genOrderSn(int $uid): string
|
||||
{
|
||||
return date('YmdHis') . $uid . mt_rand(100, 999);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use think\Request;
|
||||
use ywxapp\service\FileStorageService;
|
||||
use ywxapp\model\MemberProfile;
|
||||
use ywxapp\model\Medal;
|
||||
use app\member\validate\Profile as ProfileValidate;
|
||||
|
||||
use ywxapp\controller\MemberBase;
|
||||
/**
|
||||
* Profile 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Profile extends MemberBase
|
||||
{
|
||||
|
||||
/**
|
||||
* 不需要登录验证的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 不需要权限验证的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 个人资料页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->auth->model;
|
||||
$this->view->assign('info', $user);
|
||||
$this->view->assign('profile', $user->profile ?? new MemberProfile());
|
||||
$this->view->assign('medals', Medal::getUserMedals((int) $user->uid));
|
||||
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存个人资料
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误');
|
||||
}
|
||||
$data = $this->request->param();
|
||||
$validate = new ProfileValidate();
|
||||
if (! $validate->check($data)) {
|
||||
$this->result->error($validate->getError());
|
||||
}
|
||||
$user = $this->auth->model;
|
||||
|
||||
// 基础资料(用户表)
|
||||
$user->nickname = $data['nickname'] ?? $user->nickname;
|
||||
$user->avatar = $data['avatar'] ?? $user->avatar;
|
||||
$user->email = $data['email'] ?? $user->email;
|
||||
$user->mobile = $data['mobile'] ?? $user->mobile;
|
||||
$user->save();
|
||||
|
||||
// 扩展资料(user_profile 表)
|
||||
$profile = $user->profile;
|
||||
if (! $profile) {
|
||||
$profile = new MemberProfile();
|
||||
$profile->uid = $user->uid;
|
||||
}
|
||||
if (isset($data['gender'])) {
|
||||
$profile->gender = (int) $data['gender'];
|
||||
}
|
||||
if (isset($data['bio'])) {
|
||||
$profile->bio = $data['bio'];
|
||||
}
|
||||
$profile->save();
|
||||
|
||||
$this->result->success('保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码页
|
||||
*/
|
||||
public function password()
|
||||
{
|
||||
return $this->view->fetch('profile/password');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新密码
|
||||
*/
|
||||
public function passwordSave()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误');
|
||||
}
|
||||
$data = $this->request->param();
|
||||
$validate = new ProfileValidate();
|
||||
if (! $validate->scene('password')->check($data)) {
|
||||
$this->result->error($validate->getError());
|
||||
}
|
||||
$user = $this->auth->model;
|
||||
$old = (string) $this->request->param('old_password', '');
|
||||
$new = (string) $this->request->param('password', '');
|
||||
$new2 = (string) $this->request->param('password_confirm', '');
|
||||
|
||||
if ($new === '' || $new !== $new2) {
|
||||
$this->result->error('两次新密码不一致或为空');
|
||||
}
|
||||
if (! $user->checkPassword($old)) {
|
||||
$this->result->error('原密码错误');
|
||||
}
|
||||
$user->resetPassword($new);
|
||||
$this->result->success('密码修改成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像上传(裁剪后的图片)
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$file = $this->request->file('file');
|
||||
if (! $file || ! $file->isValid()) {
|
||||
$this->result->error('请选择上传文件', 400);
|
||||
}
|
||||
$type = $this->request->param('type', 'avatar');
|
||||
$storage = new FileStorageService();
|
||||
$result = $storage->upload($file, $type . '/' . date('Ymd'));
|
||||
$info = $this->auth->model;
|
||||
if ($info) {
|
||||
$info->avatar = $result['storage'] == 'local' ? '/storage/' . $result['path'] : $result['url'];
|
||||
$info->save();
|
||||
$this->result->success(['src' => $info->avatar], '头像上传成功');
|
||||
} else {
|
||||
$this->result->error($file->getError(), 500);
|
||||
}
|
||||
}
|
||||
return $this->view->fetch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use app\member\validate\Register as RegisterValidate;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Env;
|
||||
use think\facade\Event;
|
||||
use think\Request;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use ywxapp\library\Sms;
|
||||
use ywxapp\model\Sms as SmsModel;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\model\MemberProfile;
|
||||
use ywxapp\model\MemberGroupAccess;
|
||||
|
||||
/**
|
||||
* Register 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Register extends MemberBase
|
||||
{
|
||||
/**
|
||||
* 无需登录即可访问注册相关接口
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 注册页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->view->layout(false);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册短信验证码
|
||||
* 事件驱动:SmsSend 由短信插件实际下发;开发环境默认监听空发并回传验证码。
|
||||
*/
|
||||
public function sendsms()
|
||||
{
|
||||
if (!($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('访问错误');
|
||||
}
|
||||
$cellphone = (string)$this->request->param('cellphone', '');
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $cellphone)) {
|
||||
$this->result->error('手机号格式不正确', 1);
|
||||
}
|
||||
if (UserModel::where('mobile', $cellphone)->whereOr('account', $cellphone)->find()) {
|
||||
$this->result->error('该手机号已注册', 1);
|
||||
}
|
||||
if (!Sms::send($cellphone, null, 'register')) {
|
||||
$this->result->error('验证码发送失败', 1);
|
||||
}
|
||||
// 开发模式把验证码回传前端便于测试
|
||||
if (Env::get('app_debug')) {
|
||||
$code = SmsModel::where(['mobile' => $cellphone, 'event' => 'register'])
|
||||
->order('id', 'DESC')->value('code');
|
||||
$this->result->success(['debug_code' => $code]);
|
||||
}
|
||||
$this->result->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册提交
|
||||
* 流程:校验表单 → 校验短信验证码 → 创建用户(模型自动加盐哈希密码)
|
||||
* → 写入资料/用户组 → 触发 UserRegister / MemberLog 事件(插件钩子点)
|
||||
*/
|
||||
public function save(Request $request)
|
||||
{
|
||||
if (!($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('访问错误');
|
||||
}
|
||||
$data = $this->request->param();
|
||||
try {
|
||||
validate(RegisterValidate::class)->check($data);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage(), 1);
|
||||
}
|
||||
|
||||
// 校验短信验证码(Sms::check 比对数据库,无需监听)
|
||||
if (!Sms::check($data['cellphone'], $data['vercode'], 'register')) {
|
||||
$this->result->error('验证码错误或已过期', 1);
|
||||
}
|
||||
if (UserModel::where('mobile', $data['cellphone'])->whereOr('account', $data['cellphone'])->find()) {
|
||||
$this->result->error('该手机号已注册', 1);
|
||||
}
|
||||
|
||||
$user = UserModel::create([
|
||||
'account' => $data['cellphone'],
|
||||
'mobile' => $data['cellphone'],
|
||||
'nickname' => $data['nickname'],
|
||||
'email' => $data['cellphone'] . '@mobile.local', // 手机号注册无邮箱,写入唯一占位避免唯一键冲突
|
||||
'password' => $data['password'], // 触发模型 setPasswordAttr 自动加盐哈希
|
||||
'status' => 1,
|
||||
'gid' => 2, // 普通会员组(wxapp_user_group.id=2)
|
||||
'create_ip' => $this->request->ip(),
|
||||
'update_ip' => $this->request->ip(),
|
||||
]);
|
||||
|
||||
// 建立用户资料行与默认用户组
|
||||
MemberProfile::create(['uid' => $user->uid]);
|
||||
MemberGroupAccess::create(['uid' => $user->uid, 'gid' => 2]);
|
||||
|
||||
// ===== 插件事件触发点 =====
|
||||
// UserRegister:业务扩展钩子,插件可在此发欢迎信、初始化钱包、分配角色等
|
||||
Event::trigger('UserRegister', ['member' => $user, 'data' => $data]);
|
||||
// MemberLog:审计钩子(可监听写入 wxapp_user_log)
|
||||
Event::trigger('MemberLog', [
|
||||
'uid' => $user->uid,
|
||||
'action' => 'register',
|
||||
'ip' => $this->request->ip(),
|
||||
'remark' => '用户注册',
|
||||
]);
|
||||
|
||||
Sms::flush($data['cellphone'], 'register');
|
||||
|
||||
$this->result->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use think\facade\Event;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use app\member\validate\Settings as SettingsValidate;
|
||||
|
||||
/**
|
||||
* 账户设置
|
||||
* 解决此前「设置」菜单为死链的问题(member 应用原无 Settings 控制器)。
|
||||
* 页面展示账户基础信息,并支持更新昵称 / 邮箱,更新时触发 UserSettingUpdate 事件。
|
||||
*/
|
||||
class Settings extends MemberBase
|
||||
{
|
||||
/**
|
||||
* 需登录
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 设置页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->user();
|
||||
$this->view->assign('info', $user);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存设置
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if (!($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('访问错误');
|
||||
}
|
||||
$user = $this->user();
|
||||
$data = $this->request->param();
|
||||
$validate = new SettingsValidate();
|
||||
if (! $validate->check($data)) {
|
||||
$this->result->error($validate->getError());
|
||||
}
|
||||
$update = [];
|
||||
|
||||
if (isset($data['nickname']) && $data['nickname'] !== '') {
|
||||
$update['nickname'] = $data['nickname'];
|
||||
}
|
||||
if (isset($data['email']) && $data['email'] !== '') {
|
||||
$update['email'] = $data['email'];
|
||||
}
|
||||
|
||||
if (!empty($update)) {
|
||||
$user->save($update);
|
||||
// 插件事件触发点:账户设置更新
|
||||
Event::trigger('UserSettingUpdate', ['member' => $user, 'data' => $update]);
|
||||
}
|
||||
|
||||
$this->result->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use ywxapp\model\Task as TaskModel;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use ywxapp\model\BaseModel;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 会员任务中心
|
||||
*/
|
||||
class Task extends MemberBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
protected function initialize() {}
|
||||
|
||||
/**
|
||||
* 任务中心列表页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务列表(AJAX):返回启用任务 + 当前用户已领取状态
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
$uid = (int) ($this->auth->model->uid ?? 0);
|
||||
$tasks = TaskModel::getEnabledList();
|
||||
$claimed = TaskModel::getClaimedTaskIds($uid);
|
||||
|
||||
$list = [];
|
||||
foreach ($tasks as $t) {
|
||||
$t = $t->toArray();
|
||||
$list[] = [
|
||||
'id' => $t['id'],
|
||||
'title' => $t['title'],
|
||||
'description' => $t['description'],
|
||||
'reward_type' => $t['reward_type'],
|
||||
'reward_type_text' => TaskModel::getRewardTypeText((int) $t['reward_type']),
|
||||
'reward_num' => $t['reward_num'],
|
||||
'claimed' => in_array($t['id'], $claimed, true),
|
||||
];
|
||||
}
|
||||
$this->result->success($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取任务奖励(AJAX)
|
||||
*/
|
||||
public function claim()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误');
|
||||
}
|
||||
$uid = (int) ($this->auth->model->uid ?? 0);
|
||||
$taskId = (int) $this->request->param('task_id', 0);
|
||||
if ($uid <= 0) {
|
||||
$this->result->error('请先登录会员中心');
|
||||
}
|
||||
if ($taskId <= 0) {
|
||||
$this->result->error('任务参数错误');
|
||||
}
|
||||
[$ok, $msg] = TaskModel::claim($uid, $taskId);
|
||||
if ($ok) {
|
||||
$this->result->success([], $msg);
|
||||
}
|
||||
$this->result->error($msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的道具页
|
||||
*/
|
||||
public function myProp()
|
||||
{
|
||||
return $this->view->fetch('task/myprop');
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的道具列表(AJAX)
|
||||
*/
|
||||
public function myPropList()
|
||||
{
|
||||
$uid = (int) ($this->auth->model->uid ?? 0);
|
||||
\ywxapp\model\Task::ensureSchema(); // 确保 user_prop 关联表已就绪
|
||||
if ($uid <= 0) {
|
||||
$this->result->error('请先登录');
|
||||
}
|
||||
$list = Db::name('member_prop')
|
||||
->alias('up')
|
||||
->join('prop p', 'p.id = up.prop_id', 'left')
|
||||
->where('up.uid', $uid)
|
||||
->field('up.id, up.prop_id, up.num, up.create_at, p.title, p.icon')
|
||||
->order('up.create_at', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
$this->result->success($list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// 这是系统自动生成的event定义文件
|
||||
return [
|
||||
'bind' => [
|
||||
// 更多事件绑定
|
||||
],
|
||||
'listen' => [
|
||||
'UserLog' => ['app\member\listener\UserLog'],
|
||||
// 更多事件监听
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'My Profile' => '我的资料',
|
||||
'Settings' => '设置',
|
||||
'Sign out' => '退出登录',
|
||||
'Data Dashboard' => '数据看板',
|
||||
'Login' => '登录',
|
||||
'Register' => '注册',
|
||||
];
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'My Profile' => '我的资料',
|
||||
'Upload Image' => '上传图片',
|
||||
'Avatar' => '头像',
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\listener;
|
||||
use ywxapp\model\MemberLog as UserLogModel;
|
||||
/**
|
||||
* MemberLog 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class UserLog
|
||||
{
|
||||
/**
|
||||
* 事件监听处理
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
UserLogModel::create([
|
||||
'uid' => $event['uid'],
|
||||
'action' => $event['action'],
|
||||
'ip' => $event['ip'],
|
||||
'remark' => $event['remark'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\listener;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 会员激活监听器
|
||||
*
|
||||
* 监听 paydemo 插件在支付成功回调(PaymentNotify -> finishOrder)时
|
||||
* 触发的 UserMemberActivate 事件,完成「会员权益写入」:
|
||||
* 1. 更新用户表 vip_expire(会员到期时间,int 时间戳)、vip_level(等级)、gid(会员组)
|
||||
* 2. 写入用户组关联 wxapp_user_group_access(create_at 为 int 时间戳)
|
||||
* 3. 记录资金流水 wxapp_user_bill(created_at/updated_at 为 int 时间戳)
|
||||
* 4. 记录操作日志 wxapp_user_log(create_at 为 int 时间戳)
|
||||
*
|
||||
* 注:写入统一使用 strict(false),以兼容运行库与 SQL 文件间可能存在的字段/类型差异,
|
||||
* 真实字段正确性由 MySQL 校验。
|
||||
*
|
||||
* 事件参数:['uid'=>int, 'plan_id'=>int, 'order'=>array, 'method'=>string]
|
||||
*/
|
||||
class UserMemberActivate
|
||||
{
|
||||
|
||||
public function handle($event)
|
||||
{
|
||||
$uid = (int)($event['uid'] ?? 0);
|
||||
$planId = (int)($event['plan_id'] ?? 0);
|
||||
$order = $event['order'] ?? [];
|
||||
if ($uid <= 0 || $planId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$plans = config('member.plans', []);
|
||||
$plan = $plans[$planId] ?? null;
|
||||
if (empty($plan)) {
|
||||
Log::warning('[UserMemberActivate] 未知套餐 plan_id=' . $planId);
|
||||
return;
|
||||
}
|
||||
|
||||
$duration = (int)($plan['duration'] ?? 0);
|
||||
$level = (int)($plan['level'] ?? 0);
|
||||
$gid = (int)($plan['gid'] ?? 0);
|
||||
|
||||
$user = Db::name('member')->where('uid', $uid)->find();
|
||||
if (empty($user)) {
|
||||
Log::warning('[UserMemberActivate] 用户不存在 uid=' . $uid);
|
||||
return;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
// 在原未过期时间基础上顺延,避免重复购买被覆盖;已过期则从当前顺延
|
||||
$base = (!empty($user['vip_expire']) && $user['vip_expire'] > $now) ? $user['vip_expire'] : $now;
|
||||
$expire = $duration > 0 ? $base + $duration * 86400 : $base;
|
||||
|
||||
// 1. 用户主表(vip_expire/vip_level/update_at 为 int 时间戳)
|
||||
// 使用原生 SQL,避免运行库新增列尚未进入 ORM 字段缓存时的校验/忽略问题
|
||||
$prefix = config('database.prefix', 'wxapp_');
|
||||
$newGid = $gid > 0 ? $gid : $user['gid'];
|
||||
Db::execute("UPDATE `{$prefix}member` SET `vip_expire`=?, `vip_level`=?, `gid`=?, `update_at`=? WHERE `uid`=?", [
|
||||
$expire, $level, $newGid, $now, $uid,
|
||||
]);
|
||||
|
||||
// 2. 用户组关联(create_at 为 int 时间戳)
|
||||
if ($gid > 0) {
|
||||
$exists = Db::name('member_group_access')->where(['uid' => $uid, 'gid' => $gid])->value('uid');
|
||||
if (!$exists) {
|
||||
Db::name('member_group_access')->strict(false)->insert([
|
||||
'uid' => $uid,
|
||||
'gid' => $gid,
|
||||
'create_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 资金流水(created_at/updated_at 为 int 时间戳)
|
||||
$amount = isset($order['amount']) ? $order['amount'] : ($plan['price'] ?? 0);
|
||||
Db::name('member_bill')->strict(false)->insert([
|
||||
'uid' => $uid,
|
||||
'type' => 1,
|
||||
'amount' => $amount,
|
||||
'currency' => 1,
|
||||
'channel' => $event['method'] ?? '',
|
||||
'order_no' => $order['order_sn'] ?? '',
|
||||
'status' => 1,
|
||||
'description' => '开通' . $plan['title'] . '会员',
|
||||
'create_at' => $now,
|
||||
'update_at' => $now,
|
||||
]);
|
||||
|
||||
// 4. 操作日志(create_at 为 int 时间戳)
|
||||
Db::name('member_log')->strict(false)->insert([
|
||||
'uid' => $uid,
|
||||
'action' => 'member_activate',
|
||||
'ip' => $order['client_ip'] ?? '',
|
||||
'remark' => '开通' . $plan['title'] . '会员(订单' . ($order['order_sn'] ?? '') . '),到期 ' . date('Y-m-d H:i:s', $expire),
|
||||
'create_at' => $now,
|
||||
]);
|
||||
|
||||
Log::info('[UserMemberActivate] 用户#' . $uid . ' 开通 ' . $plan['title'] . ' 会员,到期 ' . date('Y-m-d H:i:s', $expire));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
// 会员中心(user 应用)应用级中间件。
|
||||
// 登录与权限校验已统一下沉到控制器层:MemberBase / FrontendBase 的 _initialize()
|
||||
// 调用 Auth::verifyAuth($noNeedLogin, $noNeedVerify),不再依赖 MemberAuth 中间件。
|
||||
return [
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\member\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 会员卡密兑换校验
|
||||
*/
|
||||
class Card extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'cardno' => 'require|length:6,100',
|
||||
'password' => 'require|length:4,100',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'cardno.require' => '请输入卡号',
|
||||
'cardno.length' => '卡号格式不正确',
|
||||
'password.require' => '请输入密码',
|
||||
'password.length' => '密码格式不正确',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'redeem' => ['cardno', 'password'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Login 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Login extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
'username|用户名' => 'require|max:25',
|
||||
'password|密码' => 'require',
|
||||
'captcha|验证码' => 'require|captcha',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [
|
||||
'username.require' => '请输入用户名',
|
||||
'username.max' => '名称最多不能超过25个字符',
|
||||
'password.require' => '密码错误',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Payment 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Payment extends Validate
|
||||
{
|
||||
/**
|
||||
* 创建支付订单校验
|
||||
* 字段:plan_id 套餐ID(必填正整数) / method 支付方式(付费套餐必填)
|
||||
*/
|
||||
protected $rule = [
|
||||
'plan_id' => 'require|integer|gt:0',
|
||||
'method' => 'alphaDash',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'plan_id.require' => '请选择套餐',
|
||||
'plan_id.integer' => '套餐参数错误',
|
||||
'plan_id.gt' => '套餐参数错误',
|
||||
'method.alphaDash' => '支付方式参数错误',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Profile 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Profile extends Validate
|
||||
{
|
||||
/**
|
||||
* 个人资料保存校验
|
||||
* 字段:nickname 昵称 / email 邮箱 / mobile 手机号(均选填,但填写时需合规)
|
||||
*/
|
||||
protected $rule = [
|
||||
'nickname' => 'chsDash|length:2,20',
|
||||
'email' => 'email',
|
||||
'mobile' => 'regex:/^1[3-9]\d{9}$/',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'nickname.chsDash' => '昵称只能包含中文、字母、数字及 _-',
|
||||
'nickname.length' => '昵称长度需在 2-20 位',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'mobile.regex' => '手机号格式不正确',
|
||||
];
|
||||
|
||||
/**
|
||||
* 修改密码校验
|
||||
*/
|
||||
protected $scene = [
|
||||
'password' => ['old_password' => 'require', 'password' => 'require|length:6,20', 'password_confirm' => 'require|confirm:password'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Register 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Register extends Validate
|
||||
{
|
||||
/**
|
||||
* 注册表单校验规则(与前端 register/index.html 字段一致)
|
||||
* 字段:cellphone 手机号 / vercode 短信验证码 / password / repass 确认密码 / nickname 昵称 / agreement 协议
|
||||
*/
|
||||
protected $rule = [
|
||||
'cellphone' => 'require|regex:/^1[3-9]\d{9}$/',
|
||||
'vercode' => 'require|length:4,6',
|
||||
'password' => 'require|length:6,20',
|
||||
'repass' => 'require|confirm:password',
|
||||
'nickname' => 'require|length:2,20',
|
||||
'agreement' => 'accepted',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'cellphone.require' => '请输入手机号',
|
||||
'cellphone.regex' => '手机号格式不正确',
|
||||
'vercode.require' => '请输入短信验证码',
|
||||
'password.require' => '请输入密码',
|
||||
'password.length' => '密码长度需在 6-20 位',
|
||||
'repass.confirm' => '两次输入的密码不一致',
|
||||
'nickname.require' => '请输入昵称',
|
||||
'nickname.chsDash' => '昵称只能包含中文、字母、数字及 _-',
|
||||
'agreement.accepted' => '请先同意用户协议',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\member\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Settings 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Settings extends Validate
|
||||
{
|
||||
/**
|
||||
* 账户设置保存校验(nickname/email 选填,填写时需合规)
|
||||
*/
|
||||
protected $rule = [
|
||||
'nickname' => 'chsDash|length:2,20',
|
||||
'email' => 'email',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'nickname.chsDash' => '昵称只能包含中文、字母、数字及 _-',
|
||||
'nickname.length' => '昵称长度需在 2-20 位',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<link href="/static/member/css/common.css" rel="stylesheet" />
|
||||
<style>
|
||||
.card-exchange { max-width: 520px; margin: 30px auto; }
|
||||
.card-exchange .layui-card-body { padding: 30px; }
|
||||
.card-tip { color: #999; font-size: 13px; line-height: 22px; margin-top: 10px; }
|
||||
</style>
|
||||
|
||||
<div class="card-exchange">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">卡密充值</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="cardExchangeForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">卡号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="cardno" required lay-verify="required" placeholder="请输入充值卡号" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" required lay-verify="required" placeholder="请输入充值密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="cardExchangeSubmit">立即充值</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-tip">
|
||||
说明:充值卡密为一次性使用,兑换成功后余额将实时到账,可在「账户中心」查看余额变动记录。
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['layer', 'http', 'form'], function () {
|
||||
var layer = layui.layer, http = layui.http, form = layui.form, $ = layui.$;
|
||||
|
||||
form.on('submit(cardExchangeSubmit)', function (data) {
|
||||
var field = data.field;
|
||||
http.post('/user/card/redeem', field).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.message || '兑换成功', { icon: 1 });
|
||||
$('form[lay-filter=cardExchangeForm]')[0].reset();
|
||||
form.render();
|
||||
} else {
|
||||
layer.msg(res.message || '兑换失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function (e) {
|
||||
layer.msg('请求失败:' + e, { icon: 2 });
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<link href="/static/member/css/common.css" rel="stylesheet" />
|
||||
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">我的兑换记录</div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-hide" id="recordTable" lay-filter="recordTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['table', 'http'], function () {
|
||||
var table = layui.table, http = layui.http;
|
||||
table.render({
|
||||
elem: '#recordTable',
|
||||
url: '/user/card/records',
|
||||
toolbar: ['filter', 'exports', 'print'],
|
||||
parseData: function (res) {
|
||||
return { code: res.code === 0 ? 0 : 1, msg: res.message || '', count: res.count || 0, data: res.data || [] };
|
||||
},
|
||||
cols: [[
|
||||
{ field: 'id', width: 80, title: 'ID', sort: true },
|
||||
{ field: 'cardno', minWidth: 200, title: '卡号' },
|
||||
{ field: 'amount', width: 120, title: '面值(元)', sort: true, templet: function (d) { return '¥' + parseFloat(d.amount).toFixed(2); } },
|
||||
{ field: 'use_time_text', width: 180, title: '兑换时间' },
|
||||
{ field: 'create_at', width: 180, title: '生成时间' }
|
||||
]],
|
||||
limits: [10, 15, 20, 50],
|
||||
limit: 15,
|
||||
page: true,
|
||||
skin: 'line'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!--
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-06 23:31:51
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\member\view\layout.html
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
-->
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{$title | default='YwxApp 会员中心'}</title>
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link href="/assets/layui/css/layui.css" rel="stylesheet" />
|
||||
<link href="/static/member/css/common.css" rel="stylesheet" />
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.layui-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layui-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layui-table-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layui-table-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layui-table-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script type="text/javascript">
|
||||
layui.app = {
|
||||
root: "{$site.root|default=''}",
|
||||
assetUrl: "/assets",
|
||||
module: "{$site.module}",
|
||||
app: "{$site.app}",
|
||||
routeBase: "{$route_base}",
|
||||
controller: "{$site.controller}",
|
||||
action: "{$site.action}"
|
||||
};
|
||||
</script>
|
||||
<script src="/assets/ywxapp/ywxapp.js" module="{$site.app}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{__CONTENT__}
|
||||
|
||||
<script>
|
||||
layui.use(["element", "http", "route"], function () {
|
||||
var element = layui.element;
|
||||
var http = layui.http;
|
||||
var $ = layui.$;
|
||||
$("#logout").on("click", function () {
|
||||
http.post("{:url('user/Login/logout')}").then(function (res) {
|
||||
if (res.code == 0) {
|
||||
var appName = (layui.setter && layui.setter.appName) || (layui.app && wlayui.app.module) || "user";
|
||||
layui.data(appName, { key: "access_token", remove: true });
|
||||
layui.data(appName, { key: "refresh_token", remove: true });
|
||||
layui.data(appName, { key: "menus", remove: true });
|
||||
location.href = "/";
|
||||
} else {
|
||||
layer.msg(res.msg || "退出登录失败");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
<!--
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-08-06 22:05:38
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-07 20:44:17
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\member\view\index\index.html
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Member-YwxApp</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/assets/layui/css/layui.css" rel="stylesheet">
|
||||
<!-- ywxapp.css 与 wxapp.css 内容完全一致(均含完整 .layui-body {fixed} 布局链),
|
||||
此处仅引 ywxapp.css 一份即可,避免重复加载 -->
|
||||
<link href="/assets/ywxapp/css/ywxapp.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="layui-layout-body" id="LAY_home_iframe">
|
||||
<div id="LAY_app" style="visibility: hidden">
|
||||
<div class="layui-layout layui-layout-admin">
|
||||
<div class="layui-header">
|
||||
<!-- 头部区域 -->
|
||||
<ul class="layui-nav layui-layout-left">
|
||||
<li class="layui-nav-item layadmin-flexible" lay-unselect>
|
||||
<a href="javascript:;" layadmin-event="flexible" title="侧边伸缩">
|
||||
<i class="layui-icon layui-icon-shrink-right" id="LAY_app_flexible"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="layui-nav-item" lay-unselect>
|
||||
<a href="javascript:;" layadmin-event="refresh" title="刷新">
|
||||
<i class="layui-icon layui-icon-refresh-3"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="layui-nav layui-layout-right" lay-filter="layadmin-layout-right">
|
||||
<li class="layui-nav-item layui-hide-xs" lay-unselect>
|
||||
<a href="javascript:;" layadmin-event="theme" title="主题">
|
||||
<i class="layui-icon layui-icon-theme"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="layui-nav-item layui-hide-xs" lay-unselect>
|
||||
<a href="javascript:;" layadmin-event="fullscreen" title="全屏">
|
||||
<i class="layui-icon layui-icon-screen-full"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="layui-nav-item" lay-unselect>
|
||||
<a href="javascript:;">
|
||||
<img src="{$user.avatar|default='/static/common/images/avatar.jpg'}" class="layui-nav-img">
|
||||
<cite>{$user.nickname|default='会员'}</cite>
|
||||
</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a href="javascript:;" lay-href="{:url('user/profile/index')}" lay-text="个人资料">个人资料</a></dd>
|
||||
<dd><a href="javascript:;" lay-href="{:url('user/profile/avatar')}" lay-text="头像设置">头像设置</a></dd>
|
||||
<dd><a href="javascript:;" lay-href="{:url('user/profile/password')}" lay-text="修改密码">修改密码</a></dd>
|
||||
<hr>
|
||||
<dd style="text-align: center;"><a href="javascript:;" id="logoutBtn">退出登录</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 侧边菜单 -->
|
||||
<div class="layui-side layui-side-menu">
|
||||
<div class="layui-side-scroll">
|
||||
<div class="layui-logo" lay-href="/user/index">
|
||||
<span>会员中心</span>
|
||||
</div>
|
||||
<ul class="layui-nav layui-nav-tree" lay-accordion id="LAY-system-side-menu" lay-filter="layadmin-system-side-menu">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面标签 -->
|
||||
<div class="layadmin-pagetabs" id="LAY_app_tabs">
|
||||
<div class="layui-icon layadmin-tabs-control layui-icon-prev" layadmin-event="leftPage"></div>
|
||||
<div class="layui-icon layadmin-tabs-control layui-icon-next" layadmin-event="rightPage"></div>
|
||||
<div class="layui-icon layadmin-tabs-control layui-icon-down">
|
||||
<ul class="layui-nav layadmin-tabs-select" lay-filter="layadmin-pagetabs-nav">
|
||||
<li class="layui-nav-item" lay-unselect>
|
||||
<a href="javascript:;"></a>
|
||||
<dl class="layui-nav-child layui-anim-fadein">
|
||||
<dd layadmin-event="closeThisTabs"><a href="javascript:;">关闭当前标签页</a></dd>
|
||||
<dd layadmin-event="closeOtherTabs"><a href="javascript:;">关闭其它标签页</a></dd>
|
||||
<dd layadmin-event="closeRightTabs"><a href="javascript:;">关闭右侧标签页</a></dd>
|
||||
<dd layadmin-event="closeAllTabs"><a href="javascript:;">关闭全部标签页</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="layui-tab" lay-unauto lay-allowClose="true" lay-filter="layadmin-layout-tabs">
|
||||
<ul class="layui-tab-title" id="LAY_app_tabsheader">
|
||||
<li lay-id="console/index" lay-attr="console/index" class="layui-this"><i class="layui-icon layui-icon-home"></i></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="layui-body" id="LAY_app_body">
|
||||
<div class="layadmin-tabsbody-item layui-show">
|
||||
<iframe src="/static/member/views/console/index.html" frameborder="0" class="layadmin-iframe"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 辅助元素,一般用于移动设备下遮罩 -->
|
||||
<div class="layadmin-body-shade" layadmin-event="shade"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script type="text/javascript">
|
||||
layui.app = {
|
||||
root: "{$site.root|default=''}",
|
||||
assetUrl: "/assets",
|
||||
module: "{$site.module|default='user'}",
|
||||
realModule: "{$site.app|default='member'}",
|
||||
routeBase: "{$route_base|default=''}",
|
||||
controller: "{$site.controller|default=''}",
|
||||
action: "{$site.action|default=''}",
|
||||
devToken: "{$site.devToken|default=''}",
|
||||
devAddon: "{$site.devAddon|default=''}",
|
||||
};
|
||||
</script>
|
||||
<script src="/assets/ywxapp/ywxapp.js" module="member"></script>
|
||||
|
||||
<script>
|
||||
layui.use(['index']);
|
||||
</script>
|
||||
<script>
|
||||
layui.use(['jquery'], function () {
|
||||
var $ = layui.$;
|
||||
$('#logoutBtn').on('click', function () {
|
||||
$.post('login/logout', {}, function () {
|
||||
location.href = "{:url('login/index')}";
|
||||
}, 'json').fail(function () { location.href = "{:url('login/index')}"; });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>Member-YwxApp</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/assets/layui/css/layui.css" rel="stylesheet">
|
||||
<!-- ywxapp.css 与 wxapp.css 内容完全一致(均含完整 .layui-body {fixed} 布局链),
|
||||
此处仅引 ywxapp.css 一份即可,避免重复加载 -->
|
||||
<link href="/assets/ywxapp/css/ywxapp.css" rel="stylesheet">
|
||||
<link href="/static/member/css/login.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layadmin-user-login layadmin-user-display-show" id="LAY-user-login">
|
||||
<div class="layadmin-user-login-main">
|
||||
<div class="layadmin-user-login-box layadmin-user-login-header">
|
||||
<h2>YwxApp用户管理中心</h2>
|
||||
<p> </p>
|
||||
</div>
|
||||
<div class="layadmin-user-login-box layadmin-user-login-body layui-form">
|
||||
<div class="layui-form-item">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-username" for="LAY-user-login-username"></label>
|
||||
<input type="text" name="username" id="LAY-user-login-username" lay-verify="required" placeholder="用户名" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-password" for="LAY-user-login-password"></label>
|
||||
<input type="password" name="password" id="LAY-user-login-password" lay-verify="required" placeholder="密码" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-xs7">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-vercode" for="LAY-user-login-vercode"></label>
|
||||
<input type="text" name="captcha" id="LAY-user-login-vercode" lay-verify="required" placeholder="图形验证码" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-col-xs5">
|
||||
<div style="margin-left: 10px;">
|
||||
<img class="layadmin-user-login-codeimg verification-img" lay-filter="verifyCode" src="{:url('user/ajax/verify')}" alt="点击刷新验证码" style="cursor:pointer;transition:opacity 0.3s" title="点击刷新验证码" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" style="margin-bottom: 20px;">
|
||||
<input type="checkbox" name="remember" lay-skin="primary" title="记住密码">
|
||||
<a href="#" class="layadmin-user-jump-change layadmin-link" style="margin-top: 7px;">忘记密码?</a>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="dataSubmit">登 入</button>
|
||||
</div>
|
||||
<div class="layui-trans layui-form-item layadmin-user-login-other">
|
||||
<label>社交账号登入</label>
|
||||
<a href="javascript:;"><i class="layui-icon layui-icon-login-qq"></i></a>
|
||||
<a href="javascript:;"><i class="layui-icon layui-icon-login-wechat"></i></a>
|
||||
<a href="javascript:;"><i class="layui-icon layui-icon-login-weibo"></i></a>
|
||||
|
||||
<a href="{:url('register/index')}" class="layadmin-user-jump-change layadmin-link">注册帐号</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-trans layadmin-user-login-footer"><p>© All Rights Reserved</p></div>
|
||||
<div class="ladmin-user-login-theme">
|
||||
<script type="text/html" template>
|
||||
<ul>
|
||||
<li data-theme=""><img src="{{ layui.setter.paths.base }}style/imgs/bg-none.jpg"></li>
|
||||
<li data-theme="#03152A" style="background-color: #03152A;"></li>
|
||||
<li data-theme="#2E241B" style="background-color: #2E241B;"></li>
|
||||
<li data-theme="#50314F" style="background-color: #50314F;"></li>
|
||||
<li data-theme="#344058" style="background-color: #344058;"></li>
|
||||
<li data-theme="#20222A" style="background-color: #20222A;"></li>
|
||||
</ul>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 资 源 引 入 -->
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script type="text/javascript">
|
||||
layui.app = {
|
||||
root: "{$site.root|default=''}",
|
||||
assetUrl: "/assets",
|
||||
module: "{$site.module|default='user'}",
|
||||
realModule: "{$site.app|default='member'}",
|
||||
routeBase: "{$route_base|default=''}",
|
||||
controller: "{$site.controller|default=''}",
|
||||
action: "{$site.action|default=''}",
|
||||
devToken: "{$site.devToken|default=''}",
|
||||
devAddon: "{$site.devAddon|default=''}",
|
||||
};
|
||||
</script>
|
||||
<script src="/assets/ywxapp/ywxapp.js" module="member"></script>
|
||||
<script>
|
||||
layui.use('login')
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,62 @@
|
||||
<div class="uc-wrap">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">我的勋章</div>
|
||||
<div class="layui-card-body" pad15>
|
||||
<div class="medal-grid" id="medalGrid"></div>
|
||||
<div class="medal-empty" id="medalEmpty" style="display:none;color:#999;padding:30px 0;text-align:center;">
|
||||
<i class="layui-icon layui-icon-trophy" style="font-size:40px;display:block;margin-bottom:8px;color:#ccc;"></i>
|
||||
暂无勋章,去「任务中心」完成任务即可获得
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['http', 'layer', 'jquery'], function () {
|
||||
var http = layui.http, layer = layui.layer, $ = layui.jquery;
|
||||
|
||||
function render(list) {
|
||||
var box = $('#medalGrid'), html = '';
|
||||
if (!list || !list.length) {
|
||||
$('#medalEmpty').show();
|
||||
return;
|
||||
}
|
||||
$('#medalEmpty').hide();
|
||||
list.forEach(function (m) {
|
||||
var img = m.image
|
||||
? '<img src="' + m.image + '" alt="' + m.title + '" />'
|
||||
: '<i class="layui-icon layui-icon-trophy"></i>';
|
||||
html += ''
|
||||
+ '<div class="medal-item">'
|
||||
+ '<div class="medal-icon">' + img + '</div>'
|
||||
+ '<div class="medal-title">' + m.title + '</div>'
|
||||
+ '<div class="medal-desc">' + (m.description || '') + '</div>'
|
||||
+ '<div class="medal-time">' + (m.create_time_text || '') + ' 获得</div>'
|
||||
+ '</div>';
|
||||
});
|
||||
box.html(html);
|
||||
}
|
||||
|
||||
http.get('list').then(function (res) {
|
||||
if (res.code === 0) {
|
||||
render(res.data || []);
|
||||
} else {
|
||||
layer.msg(res.message || '加载失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.msg('加载失败', { icon: 2 });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.medal-grid { display:flex; flex-wrap:wrap; gap:16px; }
|
||||
.medal-item { width:160px; padding:18px 12px; border:1px solid #f0f0f0; border-radius:10px; text-align:center; background:#fff; transition:.2s; }
|
||||
.medal-item:hover { box-shadow:0 4px 16px rgba(0,0,0,.08); transform:translateY(-2px); }
|
||||
.medal-icon { width:64px; height:64px; line-height:64px; margin:0 auto 10px; border-radius:50%; background:linear-gradient(135deg,#fff7e6,#ffe7ba); overflow:hidden; }
|
||||
.medal-icon img { width:64px; height:64px; object-fit:cover; }
|
||||
.medal-icon .layui-icon { font-size:32px; color:#fa8c16; }
|
||||
.medal-title { font-size:15px; font-weight:600; color:#333; }
|
||||
.medal-desc { font-size:12px; color:#999; margin:6px 0; line-height:1.5; min-height:34px; }
|
||||
.medal-time { font-size:11px; color:#bbb; }
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<link href="/static/member/css/common.css" rel="stylesheet" />
|
||||
<style>
|
||||
.uc-wrap { max-width: 900px; }
|
||||
.plan-card { transition: 0.2s; }
|
||||
.plan-card:hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); }
|
||||
.plan-card .layui-card-body h1 { font-size: 34px; color: #009688; margin: 6px 0 12px; }
|
||||
.plan-card li { line-height: 28px; color: #666; }
|
||||
#payMethods { margin: 10px 0; }
|
||||
</style>
|
||||
|
||||
<div class="uc-wrap">
|
||||
<h2 style="text-align: center">选择适合您的套餐</h2>
|
||||
<br />
|
||||
<div class="layui-row layui-col-space20" id="planList">
|
||||
{volist name="plans" id="plan"}
|
||||
<div class="layui-col-md4">
|
||||
<div class="layui-card plan-card">
|
||||
<div class="layui-card-header">{$plan.title}</div>
|
||||
<div class="layui-card-body">
|
||||
<h1>¥{$plan.price}<span style="font-size: 16px">/月</span></h1>
|
||||
<ul>
|
||||
<li>{$plan.desc}</li>
|
||||
</ul>
|
||||
{if $plan.price == 0}
|
||||
<button class="layui-btn layui-btn-primary layui-btn-fluid" onclick="pay({$plan.id})">当前版本</button>
|
||||
{else}
|
||||
<button class="layui-btn layui-btn-danger layui-btn-fluid" onclick="pay({$plan.id})">立即开通</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付方式选择弹窗 -->
|
||||
<div id="payModal" style="display: none; padding: 20px">
|
||||
<div id="payMethods"></div>
|
||||
<div id="payBox" style="margin-top: 15px; text-align: center"></div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||
<script>
|
||||
layui.use(['layer', 'http', 'form'], function () {
|
||||
var layer = layui.layer, http = layui.http, form = layui.form, $ = layui.$;
|
||||
|
||||
// 套餐价格映射(用于免费判定)
|
||||
var PLAN_PRICE = {};
|
||||
{volist name = "plans" id = "plan" }
|
||||
PLAN_PRICE[{ $plan.id }] = { $plan.price };
|
||||
{/volist }
|
||||
|
||||
var curPlan = 0; // 当前选择的套餐
|
||||
|
||||
// 加载可用支付方式(插件通过 PaymentMethods 事件注入)
|
||||
function loadMethods(cb) {
|
||||
http.get('methods').then(function (res) {
|
||||
if (res.code === 0 && res.data && res.data.length) {
|
||||
cb(res.data);
|
||||
} else {
|
||||
layer.msg('暂无可用支付方式,请先安装支付插件', { icon: 2 });
|
||||
cb([]);
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.msg('支付方式加载失败', { icon: 2 });
|
||||
cb([]);
|
||||
});
|
||||
}
|
||||
|
||||
// 对外暴露:点击套餐按钮触发
|
||||
window.pay = function (planId) {
|
||||
curPlan = planId;
|
||||
if (PLAN_PRICE[planId] == 0) {
|
||||
http.post('create', { plan_id: planId, method: 'free' }).then(function (res) {
|
||||
if (res.code === 0) { layer.msg('已开通' + (res.data.plan || ''), { icon: 1 }); }
|
||||
else { layer.msg(res.message || '开通失败', { icon: 2 }); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
loadMethods(function (methods) {
|
||||
if (!methods.length) return;
|
||||
var html = '<div class="layui-form">';
|
||||
methods.forEach(function (m, i) {
|
||||
html += '<input type="radio" name="payMethod" value="' + m.code + '" title="' + m.name + '" ' + (i === 0 ? 'checked' : '') + '>'
|
||||
+ '<div class="layui-word-aux" style="margin:2px 0 10px;">' + (m.desc || '') + '</div>';
|
||||
});
|
||||
html += '</div><button class="layui-btn layui-btn-fluid" id="doPay">确认支付</button>';
|
||||
$('#payMethods').html(html);
|
||||
$('#payBox').empty();
|
||||
form.render();
|
||||
layer.open({ type: 1, title: '选择支付方式', area: '360px', content: $('#payModal') });
|
||||
});
|
||||
};
|
||||
|
||||
// 确认支付
|
||||
$(document).on('click', '#doPay', function () {
|
||||
var method = $('input[name="payMethod"]:checked').val();
|
||||
http.post('create', { plan_id: curPlan, method: method }).then(function (res) {
|
||||
if (res.code !== 0) { layer.msg(res.message || '创建订单失败', { icon: 2 }); return; }
|
||||
var g = res.data || {};
|
||||
if (g.type === 'error') {
|
||||
layer.msg(g.message || '支付未配置完成,无法发起支付', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
if (g.type === 'redirect' && g.redirect_url) {
|
||||
location.href = g.redirect_url;
|
||||
} else if (g.type === 'qrcode' && g.qrcode_url) {
|
||||
$('#payBox').empty().append('<div id="qrcode" style="display:inline-block;"></div><p style="color:#f60;margin-top:8px;">请使用' + (method === 'wechat' ? '微信' : '支付宝') + '扫码支付</p>');
|
||||
new QRCode(document.getElementById('qrcode'), g.qrcode_url);
|
||||
startPoll(g.order_sn);
|
||||
} else if (g.type === 'form' && g.html) {
|
||||
$('#payBox').html(g.html);
|
||||
} else if (g.type === 'free') {
|
||||
layer.msg('开通成功', { icon: 1 });
|
||||
} else {
|
||||
layer.msg('已生成支付,请按提示完成', { icon: 1 });
|
||||
}
|
||||
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
});
|
||||
|
||||
// 轮询订单支付状态。
|
||||
// 注意:支付查询由各支付插件提供对应接口(如插件暴露 /user/payment/query),
|
||||
// 框架不写死第三方路径。下方 demo 调用的 /paydemo/pay/query 为开发占位,
|
||||
// 已在真实环境移除避免 404;接入支付插件后请在此调用插件提供的查询地址。
|
||||
function startPoll(orderSn) {
|
||||
if (window.__payTimer) { clearInterval(window.__payTimer); }
|
||||
window.__payTimer = setInterval(function () {
|
||||
// TODO: 替换为支付插件提供的订单查询接口(如 url('user/payment/query'))
|
||||
// $.post(url('user/payment/query'), { order_sn: orderSn }, function (r) {
|
||||
// if (r && r.code === 0 && r.data && r.data.paid) {
|
||||
// clearInterval(window.__payTimer);
|
||||
// window.__payTimer = null;
|
||||
// layer.msg('支付成功,会员已开通', { icon: 1 });
|
||||
// setTimeout(function () { location.reload(); }, 1200);
|
||||
// }
|
||||
// }, 'json');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<link rel="stylesheet" href="/assets/ywxapp/modules/cropper/cropper.css">
|
||||
<style>
|
||||
.readyimg { background: #f7f7f7; height: 420px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.readyimg img { max-width: 100%; max-height: 100%; display: block; }
|
||||
.img-preview { width: 150px; height: 150px; overflow: hidden; border-radius: 50%; border: 1px solid #eee; background: #f7f7f7; }
|
||||
.img-preview img { width: 100%; }
|
||||
</style>
|
||||
|
||||
<div class="uc-wrap">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-form-item">
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="pickBtn">
|
||||
<i class="layui-icon"></i> 选择图片
|
||||
</button>
|
||||
<input id="avatarImgUpload" type="file" accept="image/*" name="file" style="display:none">
|
||||
<div class="layui-form-mid layui-word-aux">支持 jpg / png / gif,建议尺寸 150x150 以上,大小 4M 以内</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md9">
|
||||
<div class="readyimg"><img id="cropImage" src=""></div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-form-mid">预览:</div>
|
||||
<div class="img-preview" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-row layui-col-space15" style="margin-top:14px;">
|
||||
<div class="layui-col-md9">
|
||||
<button type="button" class="layui-btn" id="rotateLeft"><i class="layui-icon layui-icon-left"></i> 向左旋转</button>
|
||||
<button type="button" class="layui-btn" id="rotateRight"><i class="layui-icon layui-icon-right"></i> 向右旋转</button>
|
||||
<button type="button" class="layui-btn" id="resetImg"><i class="layui-icon layui-icon-refresh"></i> 重置</button>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<button type="button" class="layui-btn layui-btn-fluid" id="confirmSave">保存修改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
layui.config({ base: '/assets/ywxapp/modules/' }).extend({ cropper: 'cropper/cropper' });
|
||||
|
||||
layui.use(['jquery', 'layer', 'cropper', 'http'], function () {
|
||||
var $ = layui.$;
|
||||
var layer = layui.layer;
|
||||
var http = layui.http;
|
||||
var $image = $('#cropImage');
|
||||
var options = { aspectRatio: 1 / 1, preview: '.img-preview', viewMode: 1 };
|
||||
var cropped = false;
|
||||
|
||||
// 选择图片
|
||||
$('#pickBtn').on('click', function () { $('#avatarImgUpload').click(); });
|
||||
$('#avatarImgUpload').on('change', function () {
|
||||
var file = this.files[0];
|
||||
if (!file) return;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
$image.attr('src', e.target.result);
|
||||
$image.off('load.crop').on('load.crop', function () {
|
||||
if (cropped) { $image.cropper('destroy'); }
|
||||
$image.cropper(options);
|
||||
cropped = true;
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
// 旋转 / 重置
|
||||
$('#rotateLeft').on('click', function () { if (cropped) $image.cropper('rotate', -15); });
|
||||
$('#rotateRight').on('click', function () { if (cropped) $image.cropper('rotate', 15); });
|
||||
$('#resetImg').on('click', function () { if (cropped) $image.cropper('reset'); });
|
||||
|
||||
// 保存:裁剪后上传
|
||||
$('#confirmSave').on('click', function () {
|
||||
if (!cropped) { layer.msg('请先选择图片', { icon: 2 }); return; }
|
||||
var canvas = $image.cropper('getCroppedCanvas', { width: 300, height: 300 });
|
||||
if (!canvas) { layer.msg('裁剪失败,请重试', { icon: 2 }); return; }
|
||||
layer.load(1, { shade: [0.3, '#fff'] });
|
||||
canvas.toBlob(function (blob) {
|
||||
var fd = new FormData();
|
||||
fd.append('file', blob, 'avatar.png');
|
||||
fd.append('type', 'avatar');
|
||||
http.post("{:url('user/profile/avatar')}", fd).then(function (res) {
|
||||
layer.closeAll('loading');
|
||||
if (res.code === 0 && res.data && res.data.src) {
|
||||
layer.msg(res.msg || '头像上传成功', { icon: 1 });
|
||||
refreshParentAvatar(res.data.src);
|
||||
} else {
|
||||
layer.msg(res.msg || '上传失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.closeAll('loading');
|
||||
layer.msg('网络错误,上传失败', { icon: 2 });
|
||||
});
|
||||
}, 'image/png');
|
||||
});
|
||||
|
||||
// 刷新会员中心顶部头像与个人资料页预览
|
||||
function refreshParentAvatar(src) {
|
||||
try {
|
||||
if (window !== top && parent.layui && parent.layui.ywxapp) {
|
||||
var ts = '?t=' + Date.now();
|
||||
parent.document.querySelectorAll('img.layui-nav-img').forEach(function (im) {
|
||||
im.src = src + ts;
|
||||
});
|
||||
var prev = parent.document.getElementById('avatarPreview');
|
||||
if (prev) { prev.src = src + ts; }
|
||||
setTimeout(function () { parent.layui.ywxapp.closeThisTabs(); }, 700);
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* 跨域等情况忽略 */ }
|
||||
setTimeout(function () { location.href = '/user/profile'; }, 700);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,137 @@
|
||||
<div class="uc-wrap">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">个人资料</div>
|
||||
<div class="layui-card-body" pad15>
|
||||
<form class="layui-form" lay-filter="profileForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">头像</label>
|
||||
<div class="layui-input-inline">
|
||||
<img id="avatarPreview" src="{$profile.avatar|default=''}" class="layui-nav-img"
|
||||
style="width:48px;height:48px;border-radius:50%;">
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux" style="padding-top:14px;">
|
||||
<a href="{:url('user/Profile/avatar')}" class="layui-btn layui-btn-xs">修改头像</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item" style="margin-bottom:0;">
|
||||
<label class="layui-form-label">我的勋章</label>
|
||||
<div class="layui-input-block">
|
||||
{if $medals}
|
||||
<div class="profile-medals">
|
||||
{volist name="medals" id="m"}
|
||||
<span class="profile-medal" title="{$m.title}({$m.create_at|date='Y-m-d'} 获得)">
|
||||
{if $m.image}
|
||||
<img src="{$m.image}" alt="{$m.title}">
|
||||
{else/}
|
||||
<i class="layui-icon layui-icon-trophy"></i>
|
||||
{/if}
|
||||
</span>
|
||||
{/volist}
|
||||
<a href="{:url('user/Medal/index')}" class="profile-medal-more">全部 ›</a>
|
||||
</div>
|
||||
{else/}
|
||||
<div class="layui-form-mid layui-word-aux">暂无勋章,去 <a href="{:url('user/Task/index')}">任务中心</a> 领取吧</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">账号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" value="{$profile.account|default=''}" class="layui-input" disabled>
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">不可修改,用于登录</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">昵称</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="nickname" value="{$profile.nickname|default=''}" lay-verify="required"
|
||||
placeholder="请输入昵称" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">手机号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="mobile" value="{$profile.mobile|default=''}" lay-verify="phone"
|
||||
placeholder="请输入手机号" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="email" value="{$profile.email|default=''}" lay-verify="email"
|
||||
placeholder="请输入邮箱" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">个人简介</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="bio" placeholder="请输入个人简介" class="layui-textarea">{$profile.bio|default=''}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="saveProfile">保存</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-card" style="margin-top:16px;">
|
||||
<div class="layui-card-header">账号安全</div>
|
||||
<div class="layui-card-body" pad15>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">登录密码</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" value="********" class="layui-input" disabled>
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux" style="padding-top:8px;">
|
||||
<a href="{:url('user/Profile/password')}" class="layui-btn layui-btn-xs">修改密码</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.profile-medals { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding-top:4px; }
|
||||
.profile-medal { display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px;
|
||||
border-radius:50%; background:linear-gradient(135deg,#fff7e6,#ffe7ba); overflow:hidden; cursor:default; }
|
||||
.profile-medal img { width:36px; height:36px; object-fit:cover; }
|
||||
.profile-medal .layui-icon { font-size:20px; color:#fa8c16; }
|
||||
.profile-medal-more { font-size:12px; color:#999; padding-left:4px; }
|
||||
.profile-medal-more:hover { color:#1e9fff; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
layui.use(['form', 'http', 'layer'], function () {
|
||||
var form = layui.form, http = layui.http, layer = layui.layer, $ = layui.$;
|
||||
|
||||
form.on('submit(saveProfile)', function (obj) {
|
||||
http.post('save', obj.field).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('保存成功', { icon: 1 });
|
||||
try {
|
||||
if (window !== top && parent.layui && parent.layui.ywxapp) {
|
||||
var nav = parent.document.querySelector('.layui-layout-admin .layui-nav-child, .user-nick');
|
||||
if (nav) { nav.textContent = obj.field.nickname; }
|
||||
}
|
||||
} catch (e) {}
|
||||
} else {
|
||||
layer.msg(res.message || res.msg || '保存失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.msg('请求失败', { icon: 2 });
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<div class="uc-wrap">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">重置登录密码</div>
|
||||
<div class="layui-card-body" pad15>
|
||||
<div class="layui-form" lay-filter="passForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">当前密码</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="password" name="old_password" lay-verify="required" placeholder="请输入当前密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">新密码</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="password" name="password" id="LAY_password" lay-verify="pass" placeholder="6-12 位,不含空格" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">确认新密码</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="password" name="password_confirm" lay-verify="repass" placeholder="请输入确认密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="setmypass">确认修改</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['form', 'layer'], function () {
|
||||
var form = layui.form, layer = layui.layer, $ = layui.$;
|
||||
|
||||
form.verify({
|
||||
pass: [/^[\S]{6,12}$/, '密码必须 6 到 12 位,且不能出现空格'],
|
||||
repass: function (value) {
|
||||
if (value !== $('#LAY_password').val()) {
|
||||
return '两次密码输入不一致';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
form.on('submit(setmypass)', function (obj) {
|
||||
$.post('/user/profile/passwordSave', obj.field, function (res) {
|
||||
if (res.code === 1 || res.code === 0) {
|
||||
layer.msg(res.msg || '修改成功', { icon: 1 });
|
||||
setTimeout(function () {
|
||||
if (window !== top && parent.layui && parent.layui.ywxapp) {
|
||||
parent.layui.ywxapp.closeThisTabs();
|
||||
} else {
|
||||
location.href = '/user/index/index';
|
||||
}
|
||||
}, 800);
|
||||
} else {
|
||||
layer.msg(res.msg || '修改失败', { icon: 2 });
|
||||
}
|
||||
}, 'json').fail(function () {
|
||||
layer.msg('请求失败', { icon: 2 });
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-08-06 22:05:38
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-07 21:22:41
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\member\view\register\index.html
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>注册</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/assets/layui/css/layui.css" rel="stylesheet">
|
||||
<link href="/assets/ywxapp/css/ywxapp.css" rel="stylesheet">
|
||||
<link href="/static/member/css/login.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layadmin-user-login layadmin-user-display-show" id="LAY-user-login" style="display: none;">
|
||||
<div class="layadmin-user-login-main">
|
||||
<div class="layadmin-user-login-box layadmin-user-login-header">
|
||||
<h2>新用户注册</h2>
|
||||
<p> </p>
|
||||
</div>
|
||||
<div class="layadmin-user-login-box layadmin-user-login-body layui-form">
|
||||
<div class="layui-form-item">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-cellphone" for="LAY-user-login-cellphone"></label>
|
||||
<input type="text" name="cellphone" id="LAY-user-login-cellphone" lay-verify="phone" placeholder="手机" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-xs7">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-vercode" for="LAY-user-login-vercode"></label>
|
||||
<input type="text" name="vercode" id="LAY-user-login-vercode" lay-verify="required" placeholder="验证码" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-col-xs5">
|
||||
<div style="margin-left: 10px;">
|
||||
<button type="button" class="layui-btn layui-btn-primary layui-btn-fluid" id="LAY-user-getsmscode">获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-password" for="LAY-user-login-password"></label>
|
||||
<input type="password" name="password" id="LAY-user-login-password" lay-verify="pass" placeholder="密码" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-password" for="LAY-user-login-repass"></label>
|
||||
<input type="password" name="repass" id="LAY-user-login-repass" lay-verify="required" placeholder="确认密码" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layadmin-user-login-icon layui-icon layui-icon-username" for="LAY-user-login-nickname"></label>
|
||||
<input type="text" name="nickname" id="LAY-user-login-nickname" lay-verify="nickname" placeholder="昵称" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<input type="checkbox" name="agreement" lay-skin="primary" title="同意用户协议" checked>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="LAY-user-reg-submit">注 册</button>
|
||||
</div>
|
||||
<div class="layui-trans layui-form-item layadmin-user-login-other">
|
||||
<label>社交账号注册</label>
|
||||
<a href="javascript:;"><i class="layui-icon layui-icon-login-qq"></i></a>
|
||||
<a href="javascript:;"><i class="layui-icon layui-icon-login-wechat"></i></a>
|
||||
<a href="javascript:;"><i class="layui-icon layui-icon-login-weibo"></i></a>
|
||||
|
||||
<a href="{:url('user/login')}" class="layadmin-user-jump-change layadmin-link layui-hide-xs">用已有帐号登入</a>
|
||||
<a href="{:url('user/login')}" class="layadmin-user-jump-change layadmin-link layui-hide-sm layui-show-xs-inline-block">登入</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-trans layadmin-user-login-footer">
|
||||
|
||||
<p>© All Rights Reserved </p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script src="/assets/ywxapp/ywxapp.js" module="member"></script>
|
||||
<script>
|
||||
layui.use(['register'], function () { });
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="uc-wrap">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">基础资料</div>
|
||||
<div class="layui-card-body" pad15>
|
||||
<form class="layui-form" lay-filter="settingsForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">账号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" value="{$info.account|default=''}" class="layui-input" disabled>
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">不可修改,用于登录</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">手机号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" value="{$info.mobile|default=''}" class="layui-input" disabled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">昵称</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="nickname" value="{$info.nickname|default=''}" lay-verify="required" placeholder="请输入昵称" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="email" value="{$info.email|default=''}" lay-verify="email" placeholder="请输入邮箱" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="saveSettings">保存</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['form', 'http', 'layer'], function () {
|
||||
var form = layui.form, http = layui.http, layer = layui.layer;
|
||||
|
||||
form.on('submit(saveSettings)', function (obj) {
|
||||
http.post('save', obj.field).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('保存成功', { icon: 1 });
|
||||
} else {
|
||||
layer.msg(res.message || '保存失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.msg('请求失败', { icon: 2 });
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<div class="uc-wrap">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">任务中心</div>
|
||||
<div class="layui-card-body" pad15>
|
||||
<div class="task-list" id="taskList">
|
||||
<div class="task-empty" id="taskEmpty" style="display:none;color:#999;padding:20px 0;text-align:center;">暂无进行中的任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['http', 'layer', 'jquery'], function () {
|
||||
var http = layui.http, layer = layui.layer, $ = layui.jquery;
|
||||
|
||||
function render(list) {
|
||||
var box = $('#taskList'), html = '';
|
||||
if (!list || !list.length) {
|
||||
$('#taskEmpty').show();
|
||||
return;
|
||||
}
|
||||
$('#taskEmpty').hide();
|
||||
list.forEach(function (t) {
|
||||
var reward = '';
|
||||
if (t.reward_type == 1) {
|
||||
reward = '<span class="task-reward score">' + t.reward_num + ' 积分</span>';
|
||||
} else if (t.reward_type == 2) {
|
||||
reward = '<span class="task-reward coin">' + t.reward_num + ' 金币</span>';
|
||||
} else if (t.reward_type == 3) {
|
||||
reward = '<span class="task-reward prop">道具 #' + t.reward_num + '</span>';
|
||||
}
|
||||
var btn = t.claimed
|
||||
? '<button class="layui-btn layui-btn-disabled layui-btn-xs" disabled>已领取</button>'
|
||||
: '<button class="layui-btn layui-btn-xs task-claim" data-id="' + t.id + '">领取奖励</button>';
|
||||
html += ''
|
||||
+ '<div class="task-item">'
|
||||
+ '<div class="task-main">'
|
||||
+ '<div class="task-title">' + t.title + '</div>'
|
||||
+ '<div class="task-desc">' + (t.description || '') + '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="task-side">'
|
||||
+ '<div class="task-reward-box">' + reward + '</div>'
|
||||
+ btn
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
});
|
||||
box.html(html);
|
||||
}
|
||||
|
||||
http.get('list').then(function (res) {
|
||||
if (res.code === 0) {
|
||||
render(res.data || []);
|
||||
} else {
|
||||
layer.msg(res.message || '加载失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.msg('加载失败', { icon: 2 });
|
||||
});
|
||||
|
||||
$('#taskList').on('click', '.task-claim', function () {
|
||||
var id = $(this).data('id'), $btn = $(this);
|
||||
$btn.attr('disabled', true);
|
||||
http.post('claim', { task_id: id }).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.message || '领取成功', { icon: 1 });
|
||||
$btn.parent().find('.task-claim').removeClass('task-claim').addClass('layui-btn-disabled').text('已领取');
|
||||
http.get('list').then(function (r) {
|
||||
if (r.code === 0) { render(r.data || []); }
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.message || '领取失败', { icon: 2 });
|
||||
$btn.attr('disabled', false);
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.msg('请求失败', { icon: 2 });
|
||||
$btn.attr('disabled', false);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.task-item { display:flex; align-items:center; justify-content:space-between; padding:14px 4px; border-bottom:1px solid #f0f0f0; }
|
||||
.task-item:last-child { border-bottom:none; }
|
||||
.task-main { flex:1; padding-right:16px; }
|
||||
.task-title { font-size:15px; font-weight:600; color:#333; }
|
||||
.task-desc { font-size:13px; color:#888; margin-top:4px; line-height:1.5; }
|
||||
.task-side { text-align:right; min-width:120px; }
|
||||
.task-reward-box { margin-bottom:8px; }
|
||||
.task-reward { display:inline-block; padding:2px 10px; border-radius:12px; font-size:12px; }
|
||||
.task-reward.score { background:#e6f0ff; color:#1e6fff; }
|
||||
.task-reward.coin { background:#fff3e0; color:#ff8c00; }
|
||||
.task-reward.prop { background:#e8f8ee; color:#1aab5b; }
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<div class="uc-wrap">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">我的道具</div>
|
||||
<div class="layui-card-body" pad15>
|
||||
<div class="myprop-list" id="mypropList">
|
||||
<div class="myprop-empty" id="mypropEmpty" style="display:none;color:#999;padding:20px 0;text-align:center;">你还没有领取任何道具,去任务中心看看吧</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['http', 'layer', 'jquery'], function () {
|
||||
var http = layui.http, layer = layui.layer, $ = layui.jquery;
|
||||
|
||||
function render(list) {
|
||||
var box = $('#mypropList'), html = '';
|
||||
if (!list || !list.length) {
|
||||
$('#mypropEmpty').show();
|
||||
return;
|
||||
}
|
||||
$('#mypropEmpty').hide();
|
||||
list.forEach(function (p) {
|
||||
var icon = p.icon
|
||||
? '<img src="' + p.icon + '" class="myprop-icon" alt="' + (p.title || '') + '">'
|
||||
: '<span class="myprop-icon myprop-icon-default">道具</span>';
|
||||
html += ''
|
||||
+ '<div class="myprop-item">'
|
||||
+ '<div class="myprop-icon-box">' + icon + '</div>'
|
||||
+ '<div class="myprop-info">'
|
||||
+ '<div class="myprop-title">' + (p.title || '道具#' + p.prop_id) + '</div>'
|
||||
+ '<div class="myprop-time">获得于 ' + (p.create_at ? new Date(p.create_at * 1000).toLocaleString() : '-') + '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="myprop-num">x' + (p.num || 1) + '</div>'
|
||||
+ '</div>';
|
||||
});
|
||||
box.html(html);
|
||||
}
|
||||
|
||||
http.get('myPropList').then(function (res) {
|
||||
if (res.code === 0) {
|
||||
render(res.data || []);
|
||||
} else {
|
||||
layer.msg(res.message || '加载失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function () {
|
||||
layer.msg('加载失败', { icon: 2 });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.myprop-item { display:flex; align-items:center; padding:12px 4px; border-bottom:1px solid #f0f0f0; }
|
||||
.myprop-item:last-child { border-bottom:none; }
|
||||
.myprop-icon-box { width:48px; height:48px; margin-right:14px; flex-shrink:0; }
|
||||
.myprop-icon { width:48px; height:48px; border-radius:8px; object-fit:cover; display:block; }
|
||||
.myprop-icon-default { width:48px; height:48px; line-height:48px; text-align:center; background:#f0f4ff; color:#1e6fff; border-radius:8px; font-size:12px; }
|
||||
.myprop-info { flex:1; }
|
||||
.myprop-title { font-size:15px; font-weight:600; color:#333; }
|
||||
.myprop-time { font-size:12px; color:#999; margin-top:4px; }
|
||||
.myprop-num { color:#ff8c00; font-weight:600; font-size:15px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user