From 36cacbaf78e510713002fcd5e3d61cece2e01421 Mon Sep 17 00:00:00 2001
From: quanwei <419654421@qq.com>
Date: Tue, 28 Oct 2025 18:43:26 +0800
Subject: [PATCH] 名片模板
---
shop_vue/src/views/plus/business/template/edit.vue | 1014 ++++++++
admin/app/shop/model/plus/business/Business.php | 12
admin/app/shop/model/plus/business/Industry.php | 75
shop_vue/src/views/plus/business/grade/index.vue | 278 ++
shop_vue/src/views/plus/business/business/index.vue | 7
mobile/pages/plus/business/components/visit-card.vue | 176 +
admin/app/api/model/plus/business/Saving.php | 10
admin/app/api/controller/plus/business/Industry.php | 58
mobile/pages/plus/business/add.vue | 495 +++
mobile/pages/plus/business/index.vue | 507 ++++
shop_vue/src/views/plus/business/industry/Add.vue | 104
admin/app/api/model/plus/business/Template.php | 7
admin/app/api/controller/plus/business/Grade.php | 17
admin/app/shop/model/plus/business/Grade.php | 96
admin/app/api/model/plus/business/Business.php | 7
admin/app/api/controller/plus/business/Template.php | 17
admin/app/shop/controller/plus/business/Template.php | 195 +
shop_vue/src/views/plus/business/template/add.vue | 993 +++++++
admin/app/api/controller/plus/business/Saving.php | 17
shop_vue/src/views/plus/business/industry/Edit.vue | 108
admin/app/shop/model/plus/business/Template.php | 12
admin/app/common/model/plus/business/Business.php | 93
admin/app/common/model/plus/business/Grade.php | 104
admin/app/api/model/plus/business/Industry.php | 9
mobile/pages/plus/business/share.vue | 390 +++
mobile/pages/plus/business/detail.vue | 448 +++
shop_vue/src/views/plus/business/industry/index.vue | 179 +
shop_vue/src/views/plus/business/index.vue | 135 +
admin/app/common/service/business/Poster.php | 431 +++
shop_vue/src/api/business.js | 55
admin/app/common/model/plus/business/Industry.php | 127 +
admin/app/common/model/plus/business/Template.php | 36
admin/app/shop/controller/plus/business/Industry.php | 115
admin/app/api/controller/plus/business/Business.php | 17
mobile/pages/plus/business/information.vue | 437 +++
admin/app/common/model/plus/business/Saving.php | 52
admin/app/shop/controller/plus/business/Business.php | 30
shop_vue/src/views/plus/business/template/index.vue | 234 +
admin/app/api/model/plus/business/Grade.php | 7
mobile/pages/plus/business/visitors.vue | 288 ++
admin/app/shop/controller/plus/business/Grade.php | 79
41 files changed, 7,471 insertions(+), 0 deletions(-)
diff --git a/admin/app/api/controller/plus/business/Business.php b/admin/app/api/controller/plus/business/Business.php
new file mode 100644
index 0000000..31a57cc
--- /dev/null
+++ b/admin/app/api/controller/plus/business/Business.php
@@ -0,0 +1,17 @@
+<?php
+namespace app\api\controller\plus\business;
+
+use app\api\controller\Controller;
+use app\api\model\plus\business\Business as BusinessModel;
+
+class Business extends Controller
+{
+ /**
+ * 获取名片列表
+ */
+ public function getList()
+ {
+ $model = new BusinessModel();
+ return $this->renderSuccess('',$model->getLists());
+ }
+}
\ No newline at end of file
diff --git a/admin/app/api/controller/plus/business/Grade.php b/admin/app/api/controller/plus/business/Grade.php
new file mode 100644
index 0000000..955bb25
--- /dev/null
+++ b/admin/app/api/controller/plus/business/Grade.php
@@ -0,0 +1,17 @@
+<?php
+namespace app\api\controller\plus\business;
+
+use app\api\controller\Controller;
+use app\api\model\plus\business\Grade as GradeModel;
+
+class Grade extends Controller
+{
+ /**
+ * 获取等级列表
+ */
+ public function getList()
+ {
+ $model = new GradeModel();
+ return $this->renderSuccess('',$model->getLists());
+ }
+}
\ No newline at end of file
diff --git a/admin/app/api/controller/plus/business/Industry.php b/admin/app/api/controller/plus/business/Industry.php
new file mode 100644
index 0000000..2dbab49
--- /dev/null
+++ b/admin/app/api/controller/plus/business/Industry.php
@@ -0,0 +1,58 @@
+<?php
+namespace app\api\controller\plus\business;
+
+use app\api\controller\Controller;
+use app\api\model\plus\business\Industry as IndustryModel;
+
+class Industry extends Controller
+{
+ /**
+ * 获取所有行业(树状结构)
+ */
+ public function getIndustryTree()
+ {
+ $tree = IndustryModel::getCacheTree();
+ return $this->renderSuccess(compact('tree'));
+ }
+
+ /**
+ * 获取所有行业列表
+ */
+ public function getIndustryList()
+ {
+ $list = IndustryModel::getCacheAll();
+ return $this->renderSuccess(compact('list'));
+ }
+
+ /**
+ * 获取行业详情
+ */
+ public function detail($industry_id)
+ {
+ $industry = IndustryModel::detail($industry_id);
+ if (!$industry) {
+ return $this->renderError('行业不存在');
+ }
+ return $this->renderSuccess(compact('industry'));
+ }
+
+ /**
+ * 获取一级行业列表
+ */
+ public function getFirstIndustry()
+ {
+ $list = IndustryModel::getFirstIndustry();
+ return $this->renderSuccess(compact('list'));
+ }
+
+ /**
+ * 根据上级ID获取子行业
+ */
+ public function getSubIndustry($parent_id = 0)
+ {
+ $model = new IndustryModel;
+ $list = $model->where('parent_id', '=', $parent_id)->where('status', '=', 1)
+ ->order(['sort' => 'asc', 'create_time' => 'asc'])->select();
+ return $this->renderSuccess(compact('list'));
+ }
+}
diff --git a/admin/app/api/controller/plus/business/Saving.php b/admin/app/api/controller/plus/business/Saving.php
new file mode 100644
index 0000000..b280126
--- /dev/null
+++ b/admin/app/api/controller/plus/business/Saving.php
@@ -0,0 +1,17 @@
+<?php
+namespace app\api\controller\plus\business;
+
+use app\api\controller\Controller;
+use app\api\model\plus\business\Saving as SavingModel;
+
+class Saving extends Controller
+{
+ /**
+ * 获取名片记录列表
+ */
+ public function getList()
+ {
+ $model = new SavingModel();
+ return $this->renderSuccess('',$model->getLists());
+ }
+}
\ No newline at end of file
diff --git a/admin/app/api/controller/plus/business/Template.php b/admin/app/api/controller/plus/business/Template.php
new file mode 100644
index 0000000..90b9a93
--- /dev/null
+++ b/admin/app/api/controller/plus/business/Template.php
@@ -0,0 +1,17 @@
+<?php
+namespace app\api\controller\plus\business;
+
+use app\api\controller\Controller;
+use app\api\model\plus\business\Template as TemplateModel;
+
+class Template extends Controller
+{
+ /**
+ * 获取模板列表
+ */
+ public function getList()
+ {
+ $model = new TemplateModel();
+ return $this->renderSuccess('',$model->getLists());
+ }
+}
diff --git a/admin/app/api/model/plus/business/Business.php b/admin/app/api/model/plus/business/Business.php
new file mode 100644
index 0000000..2926385
--- /dev/null
+++ b/admin/app/api/model/plus/business/Business.php
@@ -0,0 +1,7 @@
+<?php
+namespace app\api\model\plus\business;
+use app\common\model\plus\business\Business as CommonBusiness;
+class Business extends CommonBusiness
+{
+
+}
\ No newline at end of file
diff --git a/admin/app/api/model/plus/business/Grade.php b/admin/app/api/model/plus/business/Grade.php
new file mode 100644
index 0000000..1a53b00
--- /dev/null
+++ b/admin/app/api/model/plus/business/Grade.php
@@ -0,0 +1,7 @@
+<?php
+namespace app\api\model\plus\business;
+use app\common\model\plus\business\Grade as CommonGrade;
+class Grade extends CommonGrade
+{
+
+}
\ No newline at end of file
diff --git a/admin/app/api/model/plus/business/Industry.php b/admin/app/api/model/plus/business/Industry.php
new file mode 100644
index 0000000..b25188b
--- /dev/null
+++ b/admin/app/api/model/plus/business/Industry.php
@@ -0,0 +1,9 @@
+<?php
+namespace app\api\model\plus\business;
+
+use app\common\model\plus\business\Industry as CommonIndustry;
+
+class Industry extends CommonIndustry
+{
+
+}
diff --git a/admin/app/api/model/plus/business/Saving.php b/admin/app/api/model/plus/business/Saving.php
new file mode 100644
index 0000000..a6bad33
--- /dev/null
+++ b/admin/app/api/model/plus/business/Saving.php
@@ -0,0 +1,10 @@
+<?php
+namespace app\api\model\plus\business;
+use app\common\model\plus\business\Saving as CommonSaving;
+/**
+ * 名片记录模型
+ */
+class Saving extends CommonSaving
+{
+
+}
diff --git a/admin/app/api/model/plus/business/Template.php b/admin/app/api/model/plus/business/Template.php
new file mode 100644
index 0000000..938b3eb
--- /dev/null
+++ b/admin/app/api/model/plus/business/Template.php
@@ -0,0 +1,7 @@
+<?php
+namespace app\api\model\plus\business;
+use app\common\model\plus\business\Template as CommonTemplate;
+class Template extends CommonTemplate
+{
+
+}
diff --git a/admin/app/common/model/plus/business/Business.php b/admin/app/common/model/plus/business/Business.php
new file mode 100644
index 0000000..19f7538
--- /dev/null
+++ b/admin/app/common/model/plus/business/Business.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace app\common\model\plus\business;
+use app\common\model\BaseModel;
+
+/**
+ * 名片管理模型
+ */
+class Business extends BaseModel
+{
+ protected $name='business_card';
+
+ public function getSexAttr($value){
+ $data=[10=>'未知',20=>'男',30=>'女'];
+ return $data[$value];
+ }
+ /**
+ * 关联用户
+ * @return \think\model\relation\HasOne
+ */
+ public function user(){
+ $model=self::getCalledModule()?:'common';
+ return $this->hasOne("app\\$model\\model\\user\\User",'user_id','user_id');
+ }
+
+ /**
+ * 头像
+ * @return \think\model\relation\HasOne
+ */
+ public function image(){
+ $model=self::getCalledModule()?:'common';
+ return $this->hasOne("app\\$model\\model\\file\\UploadFile",'file_id','file_id')->bind(['file_path']);
+ }
+ /**
+ * logo
+ * @return \think\model\relation\HasOne
+ */
+ public function logoImage(){
+ $model=self::getCalledModule()?:'common';
+ return $this->hasOne("app\\$model\\model\\file\\UploadFile",'file_id','logo')->bind(['file_path']);
+ }
+ /**
+ * 添加
+ * @param $data
+ * @return false|int
+ */
+ public function add($data){
+ $data['app_id']=self::$app_id;
+ return $this->save($data);
+ }
+ public function getUnitAttr($name)
+ {
+ return json_decode($name);
+ }
+ public function getDutiesAttr($name)
+ {
+ return json_decode($name);
+ }
+ public function getAddressAttr($name)
+ {
+ return json_decode($name);
+ }
+ public function getPositionAttr($name)
+ {
+ return $name?json_decode($name):[];
+ }
+
+ /**
+ * 获取名片列表
+ * @param $param
+ * @return \think\Paginator
+ * @throws \think\db\exception\DbException
+ */
+ public function getList($param=[]){
+ $paramr=array_merge(['listRow'=>15],$param);
+ $where=[];
+ !empty($paramr['name'])&&$where['name']=['like','%'.$paramr['name'].'%'];
+ !empty($paramr['search'])&&$where['name|duties|unit']=['like','%'.$paramr['search'].'%'];
+ !empty($paramr['user_id'])&&$where['user_id']=$paramr['user_id'];
+ if(!empty($paramr['sort'])){
+ if($paramr['sort']=='name'){
+ $order=['name'=>"asc"];
+ }else if($paramr['sort']=='time'){
+ $order=['create_time'=>"asc"];
+ }else{
+ $order=['unit'=>"asc"];
+ }
+ }else{
+ $order=['is_default'=>'desc','create_time'=>'desc'];
+ }
+ return $this->with(['user','image','logoImage'])->order($order)->where($where)->paginate($paramr);
+ }
+}
\ No newline at end of file
diff --git a/admin/app/common/model/plus/business/Grade.php b/admin/app/common/model/plus/business/Grade.php
new file mode 100644
index 0000000..7674e9b
--- /dev/null
+++ b/admin/app/common/model/plus/business/Grade.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace app\common\model\plus\business;
+
+use app\common\model\BaseModel;
+
+/**
+ * 名片等级模型
+ */
+class Grade extends BaseModel
+{
+ protected $pk = 'grade_id';
+ protected $name = 'business_card_grade';
+
+ /**
+ * 名片等级模型初始化
+ */
+ public static function init()
+ {
+ parent::init();
+ }
+
+ /**
+ * 获取详情
+ */
+ public static function detail($grade_id)
+ {
+ return (new static())->find($grade_id);
+ }
+
+ /**
+ * 获取列表记录
+ */
+ public function getLists()
+ {
+ return $this->where('app_id', '=', self::$app_id)
+ ->order(['weight' => 'asc', 'create_time' => 'asc'])
+ ->select();
+ }
+
+ /**
+ * 查询列表
+ */
+ public function selectList()
+ {
+ return $this->where('app_id', '=', self::$app_id)
+ ->order(['weight' => 'asc', 'create_time' => 'asc'])
+ ->select();
+ }
+
+ /**
+ * 获取默认等级id
+ */
+ public static function getDefaultGradeId()
+ {
+ $model = new static();
+ $grade = $model->where('app_id', '=', self::$app_id)->where('weight', '=', 1)->find();
+ if (!$grade) {
+ $model->save([
+ 'name' => '默认等级',
+ 'price' => 0.00,
+ 'weight' => 1,
+ 'app_id' => self::$app_id
+ ]);
+ $grade_id = $model['grade_id'];
+ } else {
+ $grade_id = $grade['grade_id'];
+ }
+ return $grade_id;
+ }
+
+ /**
+ * 新增等级
+ */
+ public function add($data)
+ {
+ $data['app_id'] = self::$app_id;
+ return $this->save($data);
+ }
+
+ /**
+ * 编辑等级
+ */
+ public function edit($data)
+ {
+ return $this->save($data);
+ }
+
+ /**
+ * 删除等级
+ */
+ public function setDelete()
+ {
+ // 检查是否有用户使用此等级
+ if (class_exists('app\common\model\plus\business\Business')) {
+ $businessModel = new Business();
+ $count = $businessModel->where('grade_id', '=', $this['grade_id'])->count();
+ if ($count > 0) {
+ return false;
+ }
+ }
+ return $this->delete();
+ }
+}
\ No newline at end of file
diff --git a/admin/app/common/model/plus/business/Industry.php b/admin/app/common/model/plus/business/Industry.php
new file mode 100644
index 0000000..9a1219f
--- /dev/null
+++ b/admin/app/common/model/plus/business/Industry.php
@@ -0,0 +1,127 @@
+<?php
+namespace app\common\model\plus\business;
+
+use think\facade\Cache;
+use app\common\model\BaseModel;
+
+/**
+ * 行业模型
+ */
+class Industry extends BaseModel
+{
+ protected $pk = 'industry_id';
+ protected $name = 'industry';
+
+ /**
+ * 行业详情
+ */
+ public static function detail($industry_id)
+ {
+ return (new static())->find($industry_id);
+ }
+
+ /**
+ * 所有行业
+ */
+ public static function getALL()
+ {
+ $model = new static;
+ if (!Cache::get('industry_' . $model::$app_id)) {
+ $data = $model->order(['sort' => 'asc', 'create_time' => 'asc'])->select();
+ $all = !empty($data) ? $data->toArray() : [];
+ $tree = [];
+ foreach ($all as $first) {
+ if ($first['parent_id'] != 0) continue;
+ $twoTree = [];
+ foreach ($all as $two) {
+ if ($two['parent_id'] != $first['industry_id']) continue;
+ $threeTree = [];
+ foreach ($all as $three)
+ $three['parent_id'] == $two['industry_id']
+ && $threeTree[$three['industry_id']] = $three;
+ !empty($threeTree) && $two['child'] = $threeTree;
+ array_push($twoTree, $two);
+ }
+ if (!empty($twoTree)) {
+ $temp_two_tree = array_column($twoTree, 'sort');
+ array_multisort($temp_two_tree, SORT_ASC, $twoTree);
+ $first['child'] = $twoTree;
+ }
+ array_push($tree, $first);
+ }
+ Cache::tag('cache')->set('industry_' . $model::$app_id, compact('all', 'tree'));
+ }
+ return Cache::get('industry_' . $model::$app_id);
+ }
+
+ /**
+ * 获取所有行业
+ */
+ public static function getCacheAll()
+ {
+ return self::getALL()['all'];
+ }
+
+ /**
+ * 获取所有行业(树状结构)
+ */
+ public static function getCacheTree()
+ {
+ return self::getALL()['tree'];
+ }
+
+ /**
+ * 获取所有行业(树状结构)
+ * @return string
+ */
+ public static function getCacheTreeJson()
+ {
+ return json_encode(static::getCacheTree());
+ }
+
+ /**
+ * 获取指定行业下的所有子行业id
+ */
+ public static function getSubIndustryId($parent_id, $all = [])
+ {
+ $arrIds = [$parent_id];
+ empty($all) && $all = self::getCacheAll();
+ foreach ($all as $key => $item) {
+ if ($item['parent_id'] == $parent_id) {
+ unset($all[$key]);
+ $subIds = self::getSubIndustryId($item['industry_id'], $all);
+ !empty($subIds) && $arrIds = array_merge($arrIds, $subIds);
+ }
+ }
+ return $arrIds;
+ }
+
+ /**
+ * 指定的行业下是否存在子行业
+ */
+ protected static function hasSubIndustry($parentId)
+ {
+ $all = self::getCacheAll();
+ foreach ($all as $item) {
+ if ($item['parent_id'] == $parentId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 获取所有一级行业
+ */
+ public static function getFirstIndustry()
+ {
+ return (new static())->where('parent_id', '=', 0)
+ ->order(['sort' => 'asc', 'create_time' => 'asc'])
+ ->select();
+ }
+
+ public function getListByIds($ids)
+ {
+ return $this->field(['industry_id', 'name', 'parent_id'])->where('industry_id', 'in', $ids)->select();
+ }
+}
\ No newline at end of file
diff --git a/admin/app/common/model/plus/business/Saving.php b/admin/app/common/model/plus/business/Saving.php
new file mode 100644
index 0000000..540df76
--- /dev/null
+++ b/admin/app/common/model/plus/business/Saving.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace app\common\model\plus\business;
+use app\common\model\BaseModel;
+
+/**
+ * 名片保存(浏览)记录
+ */
+class Saving extends BaseModel
+{
+ protected $name='business_card_saving';
+ public function user()
+ {
+ $model=self::getCalledModule()?:'common';
+ return $this->hasOne("\\app\\$model\\model\\user\\User",'user_id','user_id');
+ }
+ public function affiliation()
+ {
+ $model=self::getCalledModule()?:'common';
+ return $this->hasOne("\\app\\$model\\model\\user\\User",'user_id','affiliation_id');
+ }
+ public function business()
+ {
+ $model=self::getCalledModule()?:'common';
+ return $this->hasOne("\\app\\$model\\model\\plus\\business\\Business",'business_card_id','business_card_id');
+ }
+
+ public function lists($param=[]){
+ $paramL=array_merge(['listRows'=>15],$param);
+ $where=[];
+ !empty($paramL['user_id'])&&$where['user_id']=$paramL['user_id'];
+ !empty($paramL['affiliation_id'])&&$where['affiliation_id']=$paramL['affiliation_id'];
+ !empty($paramL['type'])&&$where['type']=$paramL['type'];
+ return $this->with(['user'=>['businessCard'=>['image','logoImage']],'affiliation','business'=>['image']])->order('create_time','desc')->where($where)->paginate($paramL);
+ }
+
+ /**
+ * 获取记录数量
+ * @param $param
+ * @return int|string
+ * @throws \think\Exception
+ */
+ public function getQuantity($paramL=[])
+ {
+ $where=[];
+ !empty($paramL['user_id'])&&$where['user_id']=$paramL['user_id'];
+ !empty($paramL['affiliation_id'])&&$where['affiliation_id']=$paramL['affiliation_id'];
+ !empty($paramL['today'])&&$this->whereTime('create_time', 'today');
+ !empty($paramL['type'])&&$where['type']=$paramL['type'];
+ return $this->where($where)->count();
+ }
+}
\ No newline at end of file
diff --git a/admin/app/common/model/plus/business/Template.php b/admin/app/common/model/plus/business/Template.php
new file mode 100644
index 0000000..97e3ece
--- /dev/null
+++ b/admin/app/common/model/plus/business/Template.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace app\common\model\plus\business;
+
+use app\common\model\BaseModel;
+
+/**
+ * 名片模板
+ */
+class Template extends BaseModel
+{
+ protected $name='business_card_template';
+ protected $pk='template_id';
+ public function getList($param=[]){
+ $paramL=array_merge(['listRows'=>15],$param);
+ $where=[];
+ return $this->order('create_time','desc')->where($where)->paginate($paramL);
+ }
+ public function add($data){
+ $data['app_id']=self::$app_id;
+ return $this->save($data);
+ }
+
+ /**
+ * 获取模板详情
+ * @param $template_id
+ * @return array|false|\PDOStatement|string|Template
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function detail($template_id)
+ {
+ return $this->where(['template_id'=>$template_id])->find();
+ }
+}
\ No newline at end of file
diff --git a/admin/app/common/service/business/Poster.php b/admin/app/common/service/business/Poster.php
new file mode 100644
index 0000000..310e18d
--- /dev/null
+++ b/admin/app/common/service/business/Poster.php
@@ -0,0 +1,431 @@
+<?php
+
+namespace app\common\service\business;
+
+use app\common\model\plus\business\Template;
+use app\common\service\qrcode\Base;
+use Grafika\Color;
+use Grafika\Grafika;
+
+/**
+ * 生成名片
+ * Class Qrcode
+ * @package app\common\service
+ */
+class Poster extends Base
+{
+ /* @var array $dealer 用户信息 */
+ private $dealer;
+
+ /* @var array $config 名片设置 */
+ private $config;
+ public $template;
+
+ /**
+ * 构造方法
+ * Poster constructor.
+ * @param $dealer
+ * @throws \Exception
+ */
+ public function __construct($dealer)
+ {
+ parent::__construct();
+ // 用户信息
+ $this->dealer = $dealer;
+ $this->template = (new Template())->detail($dealer['template_id']);
+ // 名片设置
+ $this->config = json_decode($this->template['style'], true);
+ }
+
+ /**
+ * 获取用户名片
+ * @return string
+ * @throws \app\common\exception\BaseException
+ * @throws \think\exception\DbException
+ * @throws \Exception
+ */
+ public function getImage($isType = true)
+ {
+ if (file_exists($this->getPosterPath('business')) && $isType) {
+ return $this->getPosterUrl('business');
+ }
+ // 小程序id
+ $appId = $this->dealer['app_id'];
+ // 1. 下载背景图
+ $backdrop = $this->saveTempImage($appId, $this->config['backdrop']['src'], 'backdrop');
+ if ($this->dealer['file_path']) {
+ // 2. 下载用户头像
+ $avatarUrl = $this->saveTempImage($appId, $this->dealer['file_path'], 'avatar');
+ } else {
+ // 2. 下载用户头像
+ $avatarUrl = $this->saveTempImage($appId, $this->dealer['user']['avatarUrl'], 'avatar');
+ }
+ $logo = '';
+ if ($this->dealer['logoImage']['file_path']) {
+ // 2. 下载logo
+ $logo = $this->saveTempImage($appId, $this->dealer['logoImage']['file_path'], 'logo');
+ }
+ // 4. 拼接名片
+ return $this->savePoster($backdrop, $avatarUrl, 'business', $logo);
+ }
+
+ /**
+ * 名片图文件路径
+ * @return string
+ */
+ private function getPosterPath($name)
+ {
+ // 保存路径
+ $tempPath = $_SERVER['DOCUMENT_ROOT']. '/temp/'.$this->template['app_id'] . '/'.$name.'/';
+ !is_dir($tempPath) && mkdir($tempPath, 0755, true);
+ return $tempPath . $this->getPosterName();
+ }
+
+ /**
+ * 名片文件名称
+ * @return string
+ */
+ private function getPosterName()
+ {
+ return md5('poster_' . $this->dealer['business_card_id']) . '.png';
+ }
+
+ /**
+ * 名片url
+ * @return string
+ */
+ private function getPosterUrl($name)
+ {
+ return base_url() . '/temp/' . $this->template['app_id'] . '/' .$name . '/' . $this->getPosterName() . '?t=' . time();
+ }
+
+ /**
+ * 拼接名片
+ * @param $backdrop
+ * @param $avatarUrl
+ * @param $imageUrl
+ * @param $logo
+ * @return string
+ * @throws \Exception
+ */
+ private function savePoster($backdrop, $avatarUrl, $imageUrl, $logo)
+ {
+
+ // 创建画布
+ list($width, $height) = getimagesize($backdrop);
+ $newImage = imagecreatetruecolor($width, $height);
+ $backdropIm = $this->imagEcr($backdrop);
+ imageantialias($newImage, true);
+ // 将第一张图片覆盖在新图像上
+ imagecopy($newImage, $backdropIm, 0, 0, 0, 0, $width, $height);
+ if (!empty($this->config['icon'])) {
+ foreach ($this->config['icon'] as $key => $value) {
+ $this->addImagecopy($newImage, $value);
+ }
+ }
+
+ // 写入地址
+ //$this->addText($newImage, 'address', '地址:', 0, false, $width);
+ if ($this->config['avatar']['display'] == 1) {
+ // 生成圆形用户头像
+ $this->config['avatar']['style'] === 'circle' && $this->circular($avatarUrl, $avatarUrl);
+ // 重设用户头像宽高
+ $avatarWidth = $this->config['avatar']['width'];
+ //重设图片大小
+ $this->resizeExact($avatarUrl,$avatarWidth,$avatarWidth);
+ // 用户头像添加到背景图
+ $avatarX = $this->config['avatar']['left'];
+ $avatarY = $this->config['avatar']['top'];
+ // 打开用户头像
+ $avatarIm = $this->imagEcr($avatarUrl);
+ imagecopy($newImage, $avatarIm, $avatarX, $avatarY, 0, 0, $avatarWidth, $avatarWidth);
+ }
+ if (is_file($logo) && $this->config['logo']['display'] == 1) {
+ // 生成圆形logo
+ $this->config['logo']['style'] === 'circle' && $this->circular($logo, $logo);
+ //调整后大小
+ $logoWidth = round($this->config['logo']['width']);
+ $logoHeight = round($this->config['logo']['height']);
+ //重设图片大小
+ $this->resizeExact($logo,$logoWidth,$logoHeight);
+ // 用户logo添加到背景图
+ $logoX = $this->config['logo']['left'];
+ $logoY = $this->config['logo']['top'];
+ // 打开用户logo
+ $logoImage = $this->imagEcr($logo);
+ imagecopy($newImage, $logoImage, $logoX, $logoY, 0, 0, $logoWidth, $logoHeight);
+ }
+
+ // 写入用户昵称
+ $this->addText($newImage, 'name');
+ foreach ($this->dealer['unit'] as $key => $value) {
+ // 写入公司
+ $this->addText($newImage, 'unit', '', $key, true);
+ }
+ foreach ($this->dealer['address'] as $key => $value) {
+ // 写入地址
+ $this->addText($newImage, 'address', '', $key, true);
+ }
+ foreach ($this->dealer['duties'] as $key => $value) {
+ // 写入职务
+ $this->addText($newImage, 'duties', '', $key, true);
+ }
+ // 写入职位
+ //$this->addText($newImage, 'duties');
+ if (!empty($this->dealer['position']) && !empty($this->config['position'])) {
+ foreach ($this->dealer['position'] as $key => $value) {
+ // 写入公司
+ $this->addText($newImage, 'position', '', $key, true);
+ }
+ }
+ // 写入手机号
+ $this->addText($newImage, 'mobile', '手机:');
+
+ // 写入微信
+ if ($this->dealer['mailbox']) {
+ // 写入邮箱
+ $this->addText($newImage, 'mailbox', '邮箱:');
+ }
+ if ($this->dealer['phone']) {
+ $this->addText($newImage, 'phone', '电话:');
+ }
+ // 保存图片
+ imagejpeg($newImage, $this->getPosterPath($imageUrl),100); // 根据需要选择合适的函数(如imagepng、imagegif等)
+ // 清理内存
+ imagedestroy($newImage);
+ return $this->getPosterUrl($imageUrl);
+ }
+
+ /**
+ * 打开图片
+ * @param $image
+ * @return false|resource
+ */
+ public function imagEcr($image)
+ {
+ // 创建画布
+ return imagecreatefromstring(file_get_contents($image));
+ }
+
+ /**
+ * 重设图片大小
+ * @param $image
+ * @param $newWidth
+ * @param $newHeight
+ * @return void
+ */
+ public function resizeExact($imageUrl, $newWidth, $newHeight )
+ {
+ $image=imagecreatefromstring(file_get_contents($imageUrl));
+ // 调整图片大小
+ $targetImage = imagecreatetruecolor($newWidth, $newHeight);
+ imageantialias($targetImage, true);
+ //获取图片大小
+ $width = imagesx($image);
+ $height = imagesy($image);
+ // 这一句一定要有
+ imagesavealpha($targetImage, true);
+ $bg = imagecolorallocatealpha($targetImage, 255, 255, 255, 127);
+ imagefill($targetImage, 0, 0, $bg);
+ imagecopyresampled($targetImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
+ imagepng($targetImage, $imageUrl,0); // 根据需要选择合适的函数(如imagepng、imagegif等)
+ // 清理内存
+ imagedestroy($targetImage);
+ }
+
+ /**
+ * 添加图标
+ * @param $newImage
+ * @param $data
+ * @return void
+ */
+ public function addImagecopy($newImage,$data){
+ $src=$this->saveTempImage($this->template['app_id'], $data['src'], 'icon');
+ $this->resizeExact($src,$data['width'], $data['height']);
+ $backdropIm = $this->imagEcr($src);
+ // 将第一张图片覆盖在新图像上
+ imagecopy($newImage, $backdropIm, $data['left'], $data['top'], 0, 0, $data['width'], $data['height']);
+ }
+
+ /**
+ * 写入文字
+ * @param $editor
+ * @param $backdropImage
+ * @param $name
+ * @param $text
+ * @param $key
+ * @param $type
+ * @param $width
+ * @return array|bool
+ */
+ public function addText($editor, $name, $text = '', $key = 0, $type = false, $width = 0)
+ {
+ $fontPath = $_SERVER['DOCUMENT_ROOT']. '/fonts/MSYH.TTC';
+ if ($type) {
+ list($fontSize, $fontX,$fontY) = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
+ $colorResource=self::colorResource($this->config[$name][$key]['color'],$editor);
+ return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $this->dealer[$name][$key]);
+ } else if ($name == 'mobile') {
+ list($fontSize, $fontX,$fontY) = self::SizeLeftTop($this->config[$name]['fontSize'], $this->config[$name]['left'],$this->config[$name]['top']);
+ $text = $text . $this->dealer['mobile'];
+ if (!empty($this->dealer['mobile_phone'])) {
+ $text = $text . ' / ' . $this->dealer['mobile_phone'];
+ }
+ $colorResource=self::colorResource($this->config[$name]['color'],$editor);
+ return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text);
+ } /*else if ($name == 'duties') {
+ list($fontSize, $fontX,$fontY) = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
+ $duties = $this->dealer['duties'][$key];
+ if (!empty($this->dealer['duties'][1])) {
+ $duties = $duties . ' / ' . $this->dealer['duties'][1];
+ }
+ if (!empty($this->dealer['duties'][2])) {
+ $duties = $duties . ' / ' . $this->dealer['duties'][2];
+ }
+ $colorResource=self::colorResource($this->config['duties'][$key]['color'],$editor);
+ return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $duties);
+ }*/ else if ($name == 'address') {
+ list($fontSize, $fontX,$fontY) = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
+ $title = $this->dealer['address'][$key];
+ $strlen = mb_strlen($title, 'utf-8');
+ $left = $width - $this->config['address'][$key]['left'] - $fontSize;
+ $titleNum = bcdiv($left, $this->config['address'][$key]['fontSize']);
+ $the_box = $this->config['address'][$key]['fontSize'] * 3;
+ while ($width < ($titleNum * $this->config['address'][$key]['fontSize'] + $the_box + $fontX)) {
+ $titleNum--;
+ };
+ if ($strlen > $titleNum && $titleNum) {
+ $strArr = self::mbStrSplit($title, $titleNum);
+ }
+ $colorResource=self::colorResource($this->config['address'][$key]['color'],$editor);
+ if ($strlen > $titleNum && $titleNum) {
+ $y = $fontY + 10;
+ foreach ($strArr as $k => $v) {
+ if ($k == 0) {
+ imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $v);
+ } else {
+ $y = $y + ($fontSize * $k);
+ imagettftext($editor, $fontSize, 0, $fontX + $the_box, $y, $colorResource, $fontPath, $v);
+ }
+ }
+ return true;
+ } else {
+ return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $title);
+ }
+
+ } else {
+ list($fontSize, $fontX,$fontY) = self::SizeLeftTop($this->config[$name]['fontSize'], $this->config[$name]['left'],$this->config[$name]['top']);
+ $colorResource=self::colorResource($this->config[$name]['color'],$editor);
+ return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $this->dealer[$name]); // path/to/font.ttf为自定义字体路径
+ }
+
+ }
+
+ /**
+ * 分割字符串为数组模式
+ * @param $string
+ * @param $len
+ * @return mixed
+ */
+ private function mbStrSplit($string, $len = 1)
+ {
+ $start = 0;
+ $strlen = mb_strlen($string);
+ while ($strlen) {
+ $array[] = mb_substr($string, $start, $len, "utf8");
+ $string = mb_substr($string, $len, $strlen, "utf8");
+ $strlen = mb_strlen($string);
+ }
+ return $array;
+ }
+
+ /**
+ * 生成圆形图片
+ * @param static $imgpath 图片地址
+ * @param string $saveName 保存文件名,默认空。
+ */
+ private function circular($imgpath, $saveName = '')
+ {
+ $srcImg = imagecreatefromstring(file_get_contents($imgpath));
+ $w = imagesx($srcImg);
+ $h = imagesy($srcImg);
+ $w = $h = min($w, $h);
+ $newImg = imagecreatetruecolor($w, $h);
+ // 这一句一定要有
+ imagesavealpha($newImg, true);
+ // 拾取一个完全透明的颜色,最后一个参数127为全透明
+ $bg = imagecolorallocatealpha($newImg, 255, 255, 255, 127);
+ imagefill($newImg, 0, 0, $bg);
+ $r = $w / 2; //圆半径
+ for ($x = 0; $x < $w; $x++) {
+ for ($y = 0; $y < $h; $y++) {
+ $rgbColor = imagecolorat($srcImg, $x, $y);
+ if (((($x - $r) * ($x - $r) + ($y - $r) * ($y - $r)) < ($r * $r))) {
+ imagesetpixel($newImg, $x, $y, $rgbColor);
+ }
+ }
+ }
+ // 输出图片到文件
+ imagepng($newImg, $saveName,0);
+ // 释放空间
+ imagedestroy($srcImg);
+ imagedestroy($newImg);
+ }
+
+ /**
+ * 获取名片模板
+ * @return string
+ * @throws \app\common\exception\BaseException
+ * @throws \think\exception\DbException
+ * @throws \Exception
+ */
+ public function getImageE($config, $template_id = '')
+ {
+ $model = new Template();
+ $this->config = $config;
+ // 小程序id
+ $appId = $this->template['app_id'];
+ // 1. 下载背景图
+ $backdrop = $this->saveTempImage($appId, $this->config['backdrop']['src'], 'backdrop');
+ // 2. 下载用户头像
+ $avatarUrl = $this->saveTempImage($appId, $model::$base_url."/image/agent/avatar.jpg", 'avatar', $template_id);
+ // 2. 下载logo
+ $logo = $this->saveTempImage($appId, $this->config['logo']['src'], 'logo');
+ // 4. 拼接名片
+ return $this->savePoster($backdrop, $avatarUrl, 'template', $logo);
+ }
+
+ /**
+ * 获取颜色
+ * @param $color
+ * @param $editor
+ * @return false|int
+ */
+ private function colorResource($color, $editor)
+ {
+ $Color = new Color($color);
+ list($r, $g, $b, $alpha) = $Color->getRgba();
+ $scale = round(127 * $alpha);
+ $invert = 127 - $scale;
+ return imagecolorallocatealpha(
+ $editor,
+ $r, $g, $b,
+ $invert
+ );
+ }
+
+ /**
+ *计算字体大小和顶部跟左边的距离
+ * @param $fontSize
+ * @param $left
+ * @param $top
+ * @return array
+ */
+ private function SizeLeftTop($fontSize,$left,$top){
+ $data[0] = $fontSize * 0.76;
+ $data[1] = $left;
+ $data[2] = $top + $fontSize;
+ return $data;
+ }
+
+}
\ No newline at end of file
diff --git a/admin/app/shop/controller/plus/business/Business.php b/admin/app/shop/controller/plus/business/Business.php
new file mode 100644
index 0000000..5c70cc8
--- /dev/null
+++ b/admin/app/shop/controller/plus/business/Business.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace app\shop\controller\plus\business;
+use app\shop\controller\Controller;
+use app\shop\model\plus\business\Business as BusinessModel;
+/**
+ * 名片管理
+ */
+class Business extends Controller
+{
+ public function index(){
+ $list=(new BusinessModel)->getList(request()->request());
+ return $this->renderSuccess('',compact('list'));
+ }
+ public function edit(){
+ $param=request()->param();$param=$param['business'];
+ $model=(new BusinessModel())->get($param['business_card_id']);
+ if($model->add($param)){
+ return $this->renderSuccess('编辑成功');
+ }
+ return $this->renderError('编辑失败');
+ }
+ public function delete(){
+ $param=request()->param();
+ if((new BusinessModel())->where('business_card_id',$param['business_card_id'])->delete()){
+ return $this->renderSuccess('删除成功');
+ }
+ return $this->renderError('删除失败');
+ }
+}
\ No newline at end of file
diff --git a/admin/app/shop/controller/plus/business/Grade.php b/admin/app/shop/controller/plus/business/Grade.php
new file mode 100644
index 0000000..ce207a7
--- /dev/null
+++ b/admin/app/shop/controller/plus/business/Grade.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace app\shop\controller\plus\business;
+
+use app\shop\model\plus\business\Grade as GradeModel;
+use app\shop\controller\Controller;
+
+/**
+ * 名片等级控制器
+ */
+class Grade extends Controller
+{
+ /**
+ * 名片等级列表
+ */
+ public function index()
+ {
+ $model = new GradeModel;
+ $list = $model->getList($this->postData());
+ return $this->renderSuccess('', compact('list'));
+ }
+
+ /**
+ * 添加等级
+ */
+ public function add()
+ {
+ $model = new GradeModel;
+ // 新增记录
+ if ($model->add($this->postData())) {
+ return $this->renderSuccess('添加成功');
+ }
+ return $this->renderError($model->getError() ?: '添加失败');
+ }
+
+ /**
+ * 编辑等级
+ */
+ public function edit($grade_id)
+ {
+ $model = GradeModel::detail($grade_id);
+ if (!$model) {
+ return $this->renderError('等级不存在');
+ }
+ // 修改记录
+ if ($model->edit($this->postData())) {
+ return $this->renderSuccess('修改成功');
+ }
+ return $this->renderError($model->getError() ?: '修改失败');
+ }
+
+ /**
+ * 删除等级
+ */
+ public function delete($grade_id)
+ {
+ // 等级详情
+ $model = GradeModel::detail($grade_id);
+ if (!$model) {
+ return $this->renderError('等级不存在');
+ }
+ if (!$model->setDelete()) {
+ return $this->renderError('已存在使用此等级的名片,删除失败');
+ }
+ return $this->renderSuccess('删除成功');
+ }
+
+ /**
+ * 获取等级详情
+ */
+ public function detail($grade_id)
+ {
+ $model = GradeModel::detail($grade_id);
+ if (!$model) {
+ return $this->renderError('等级不存在');
+ }
+ return $this->renderSuccess('', compact('model'));
+ }
+}
\ No newline at end of file
diff --git a/admin/app/shop/controller/plus/business/Industry.php b/admin/app/shop/controller/plus/business/Industry.php
new file mode 100644
index 0000000..b076413
--- /dev/null
+++ b/admin/app/shop/controller/plus/business/Industry.php
@@ -0,0 +1,115 @@
+<?php
+namespace app\shop\controller\plus\business;
+
+use app\shop\controller\Controller;
+use app\shop\model\plus\business\Industry as IndustryModel;
+use think\facade\Cache;
+
+class Industry extends Controller
+{
+ /**
+ * 行业列表
+ */
+ public function index()
+ {
+ $model = new IndustryModel;
+ $list = $model->getALL();
+ return $this->renderSuccess('',compact('list'));
+ }
+
+ /**
+ * 行业详情
+ */
+ public function detail($industry_id)
+ {
+ $industry = IndustryModel::detail($industry_id);
+ if (!$industry) {
+ return $this->renderError('行业不存在');
+ }
+ return $this->renderSuccess(compact('industry'));
+ }
+
+ /**
+ * 添加行业
+ */
+ public function add()
+ {
+ $model = new IndustryModel;
+ // 获取post数据
+ $data = $this->request->post();
+ // 添加行业
+ if ($model->add($data)) {
+ // 清理缓存
+ Cache::tag('cache')->clear();
+ return $this->renderSuccess('添加成功');
+ }
+ return $this->renderError($model->getError() ?: '添加失败');
+ }
+
+ /**
+ * 编辑行业
+ */
+ public function edit($industry_id)
+ {
+ // 行业详情
+ $industry = IndustryModel::detail($industry_id);
+ if (!$industry) {
+ return $this->renderError('行业不存在');
+ }
+ // 验证表单
+ if (!$this->request->isPost()) {
+ return $this->renderError('请求方式错误');
+ }
+ // 获取post数据
+ $data = $this->request->post();
+ // 编辑行业
+ if ($industry->edit($data)) {
+ // 清理缓存
+ Cache::tag('cache')->clear();
+ return $this->renderSuccess('编辑成功');
+ }
+ return $this->renderError($industry->getError() ?: '编辑失败');
+ }
+
+ /**
+ * 删除行业
+ */
+ public function delete($industry_id)
+ {
+ // 行业详情
+ $industry = IndustryModel::detail($industry_id);
+ if (!$industry) {
+ return $this->renderError('行业不存在');
+ }
+ // 检查是否有子行业
+ if (IndustryModel::hasSubIndustry($industry_id)) {
+ return $this->renderError('该行业下存在子行业,无法删除');
+ }
+ // 删除行业
+ if ($industry->delete()) {
+ // 清理缓存
+ Cache::tag('cache')->clear();
+ return $this->renderSuccess('删除成功');
+ }
+ return $this->renderError('删除失败');
+ }
+
+ /**
+ * 获取一级行业列表
+ */
+ public function getFirstIndustry()
+ {
+ $list = IndustryModel::getFirstIndustry();
+ return $this->renderSuccess(compact('list'));
+ }
+
+ /**
+ * 根据上级ID获取子行业
+ */
+ public function getSubIndustry($parent_id = 0)
+ {
+ $model = new IndustryModel;
+ $list = $model->where('parent_id', '=', $parent_id)->order(['sort' => 'asc', 'create_time' => 'asc'])->select();
+ return $this->renderSuccess(compact('list'));
+ }
+}
\ No newline at end of file
diff --git a/admin/app/shop/controller/plus/business/Template.php b/admin/app/shop/controller/plus/business/Template.php
new file mode 100644
index 0000000..dd01ca6
--- /dev/null
+++ b/admin/app/shop/controller/plus/business/Template.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace app\shop\controller\plus\business;
+
+use app\common\service\business\Poster;
+use app\shop\model\plus\business\Template as BusinessTemplate;
+use app\shop\controller\Controller;
+
+class Template extends Controller
+{
+ public function index()
+ {
+ $list = (new BusinessTemplate())->getList();
+ return $this->renderSuccess('', compact('list'));
+ }
+
+ public function edit()
+ {
+ $template_id = input('template_id');
+ $model = (new BusinessTemplate())->where('template_id', $template_id)->find();
+ if (request()->isGet()) {
+ $data = json_decode($model['style'], true);
+ empty($data['position']) ? $data['position'] = [] : '';
+ empty($data['is_business']) ? $data['is_business'] = 0 : '';
+ empty($data['positionNum']) ? $data['positionNum'] = 0 : '';
+ empty($data['icon']) ? $data['icon'] = [] : '';
+ $data = json_encode($data, JSON_UNESCAPED_UNICODE);
+ return $this->renderSuccess('', compact('data'));
+ }
+ $dealer = ['business_card_id' => $template_id, 'name' => 'XXX', 'unit' => [], 'duties' => [], 'address' => [], 'mobile' => 'xxxxxxxxxxx', 'wechat' => 'xxxxxxxxxxx', 'mailbox' => 'xxxxxxxxxxx@.xxx.com', 'phone' => 'xxx-xxx-xxx', 'website' => 'xxxxxxxxxxxxxx.com', 'fax' => 'xxx-xxx', 'zip_code' => 'xxxxxx', 'template_id' => $template_id, 'wxapp_id' => 10001];
+ $param = request()->param();
+ $imageInfo = getimagesize($param['template']['backdrop']['src']);
+ $param['template']['backdrop']['height'] = $imageInfo[1];
+ $param['template']['backdrop']['width'] = $imageInfo[0];
+ foreach ($param['template']['unit'] as $key => $value) {
+ // 写入公司
+ $dealer['unit'][] = 'xxxxx公司' . ($key + 1);
+ }
+ // 写入职位
+ //$dealer['duties'][] = '职位';
+ foreach ($param['template']['duties'] as $key => $value) {
+ // 写入职位
+ $dealer['duties'][] = '职位'.($key + 1);
+ }
+
+ foreach ($param['template']['address'] as $key => $value) {
+ // 写入地址
+ $dealer['address'][] = '广西壮族自治区南宁市江南区壮锦大道八桂绿城·龙湖御景-A栋-2单元'.($key + 1).'号';
+ }
+
+ $Qrcode = new Poster($dealer);
+ $paramL['image'] = $Qrcode->getImageE($param['template'], $template_id);
+ $paramL['style'] = json_encode($param['template'], JSON_UNESCAPED_UNICODE);
+ if ($model->add($paramL)) {
+ return $this->renderSuccess('编辑成功', url('business.template/index'));
+ }
+ return $this->renderError('编辑失败');
+ }
+
+ public function add()
+ {
+
+ $model = new BusinessTemplate();
+ if (request()->isGet()) {
+ $data = ["backdrop" => [
+ "src" => $model::$base_url."/image/agent/agent-bg.jpg",
+ 'type' => 'backdrop'
+ ],
+ "is_business" => 0,
+ "name" => [
+ "fontSize" => 14,
+ "color" => "#000000",
+ "left" => 232,
+ "top" => 13,
+ "fontWeight" => 400,
+ 'type' => 'text'
+ ],
+ "avatar" => [
+ "width" => 70,
+ "style" => "circle",
+ "left" => 37,
+ "top" => 37,
+ "display" => 1,
+ "src" => $model::$base_url."/image/agent/avatar.jpg",
+ 'type' => 'avatar'
+ ],
+ "logo" => [
+ "width" => 70,
+ "height" => 70,
+ "style" => "square",
+ "left" => 22,
+ "src" => $model::$base_url."/image/diy/logo_top.png",
+ "top" => 140,
+ "display" => 1,
+ 'type' => 'image'
+ ],
+ "mobile" => [
+ "fontSize" => 14,
+ "color" => "#000000",
+ "left" => 192,
+ "top" => 43,
+ "fontWeight" => 400
+ ],
+ "address" => [
+ ["fontSize" => 14,
+ "color" => "#000000",
+ "left" => 133,
+ "top" => 206,
+ "fontWeight" => 400,
+ 'type' => 'text']
+ ],
+ "unit" => [
+ ["fontSize" => 14,
+ "color" => "#000000",
+ "left" => 133,
+ "top" => 167,
+ "fontWeight" => 100,
+ 'type' => 'text']
+ ],
+ "duties" => [
+ ["fontSize" => 14,
+ "color" => "#000000",
+ "left" => 260,
+ "top" => 167,
+ "fontWeight" => 400,
+ 'type' => 'text']
+ ],
+ "position" => [
+ ],
+ "wechat" => [
+ "fontSize" => 14,
+ "color" => "#000000",
+ "left" => 205,
+ "top" => 65,
+ "fontWeight" => 400,
+ 'type' => 'text'
+ ],
+ "mailbox" => [
+ "fontSize" => 14,
+ "color" => "#000000",
+ "left" => 205,
+ "top" => 104,
+ "fontWeight" => 400,
+ 'type' => 'text'
+ ],
+ "phone" => [
+ "fontSize" => 14,
+ "color" => "#000000",
+ "left" => 205,
+ "top" => 84,
+ "fontWeight" => 400,
+ 'type' => 'text'
+ ],
+ 'positionNum' => 0,
+ "iconL" => [],
+
+ ];
+ return $this->renderSuccess('', [
+ 'data' => json_encode($data, JSON_UNESCAPED_UNICODE)
+ ]);
+ }
+ $param = request()->param();
+ $imageInfo = getimagesize($param['template']['backdrop']['src']);
+ $param['template']['backdrop']['height'] = $imageInfo[1];
+ $param['template']['backdrop']['width'] = $imageInfo[0];
+ $paramL['style'] = json_encode($param['template'], JSON_UNESCAPED_UNICODE);
+ if ($model->add($paramL)) {
+ $template_id = $model->template_id;
+ $dealer = ['business_card_id' => $template_id, 'name' => 'XXX', 'unit' => [], 'duties' => [], 'address' => [], 'mobile' => 'xxxxxxxxxxx', 'wechat' => 'xxxxxxxxxxx', 'mailbox' => 'Xxxxxxxxxxxxxxxxx', 'phone' => 'xxx-xxx-xxx', 'website' => 'xxxxxxxxxxxxxx.com', 'fax' => 'xxx-xxx', 'zip_code' => 'xxxxxx', 'template_id' => $template_id, 'wxapp_id' => 10001];
+ foreach ($param['template']['unit'] as $key => $value) {
+ // 写入公司
+ $dealer['unit'][] = 'xxxxx公司' . ($key + 1);
+ }
+ // 写入职位
+ $dealer['duties'][] = '职位';
+ // 写入地址
+ $dealer['address'][] = '地址1号';
+ $Qrcode = new Poster($dealer);
+ $paramI['image'] = $Qrcode->getImageE($param['template'], $template_id);
+ $modelBusiness = (new BusinessTemplate())->where(['template_id' => $template_id])->find();
+ $modelBusiness->where(['template_id' => $template_id])->update($paramI);
+ return $this->renderSuccess('添加成功', url('business.template/index'));
+ }
+ return $this->renderError('添加失败');
+ }
+
+ public function delete($template_id)
+ {
+ if ((new BusinessTemplate())->where(['template_id' => $template_id])->delete()) {
+ return $this->renderSuccess('删除成功');
+ }
+ return $this->renderError('删除失败');
+ }
+
+}
\ No newline at end of file
diff --git a/admin/app/shop/model/plus/business/Business.php b/admin/app/shop/model/plus/business/Business.php
new file mode 100644
index 0000000..1e3444b
--- /dev/null
+++ b/admin/app/shop/model/plus/business/Business.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace app\shop\model\plus\business;
+use app\common\model\plus\business\Business as BusinessModel;
+
+/**
+ * 名片管理模型
+ */
+class Business extends BusinessModel
+{
+
+}
\ No newline at end of file
diff --git a/admin/app/shop/model/plus/business/Grade.php b/admin/app/shop/model/plus/business/Grade.php
new file mode 100644
index 0000000..48d2061
--- /dev/null
+++ b/admin/app/shop/model/plus/business/Grade.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace app\shop\model\plus\business;
+
+use app\common\model\plus\business\Grade as GradeModel;
+
+/**
+ * 名片等级模型
+ */
+class Grade extends GradeModel
+{
+ /**
+ * 获取列表记录
+ */
+ public function getList($data = [])
+ {
+ $list = $this->selectList();
+ // 如果为空,则插入默认等级
+ if(count($list) == 0) {
+ $this->save([
+ 'name' => '默认等级',
+ 'price' => 0.00,
+ 'weight' => 1,
+ 'app_id' => self::$app_id,
+ 'create_time' => time(),
+ 'update_time' => time()
+ ]);
+ $list = $this->selectList();
+ }
+ return $list;
+ }
+
+ /**
+ * 新增等级
+ */
+ public function add($data)
+ {
+ // 验证数据
+ if(empty($data['name'])) {
+ $this->error = '等级名称不能为空';
+ return false;
+ }
+ if(!isset($data['price']) || $data['price'] === '') {
+ $data['price'] = 0.00;
+ } else {
+ $data['price'] = floatval($data['price']);
+ if($data['price'] < 0) {
+ $this->error = '查看联系方式价格不能小于0';
+ return false;
+ }
+ }
+ if(!isset($data['weight']) || $data['weight'] === '') {
+ $data['weight'] = 100;
+ } else {
+ $data['weight'] = intval($data['weight']);
+ if($data['weight'] < 0) {
+ $this->error = '权重不能小于0';
+ return false;
+ }
+ }
+
+ return parent::add($data);
+ }
+
+ /**
+ * 编辑等级
+ */
+ public function edit($data)
+ {
+ // 验证数据
+ if(empty($data['name'])) {
+ $this->error = '等级名称不能为空';
+ return false;
+ }
+ if(!isset($data['price']) || $data['price'] === '') {
+ $data['price'] = 0.00;
+ } else {
+ $data['price'] = floatval($data['price']);
+ if($data['price'] < 0) {
+ $this->error = '查看联系方式价格不能小于0';
+ return false;
+ }
+ }
+ if(!isset($data['weight']) || $data['weight'] === '') {
+ $data['weight'] = 100;
+ } else {
+ $data['weight'] = intval($data['weight']);
+ if($data['weight'] < 0) {
+ $this->error = '权重不能小于0';
+ return false;
+ }
+ }
+
+ return parent::edit($data);
+ }
+}
diff --git a/admin/app/shop/model/plus/business/Industry.php b/admin/app/shop/model/plus/business/Industry.php
new file mode 100644
index 0000000..5a0ff4b
--- /dev/null
+++ b/admin/app/shop/model/plus/business/Industry.php
@@ -0,0 +1,75 @@
+<?php
+namespace app\shop\model\plus\business;
+
+use app\common\model\plus\business\Industry as CommonIndustry;
+use think\facade\Cache;
+
+class Industry extends CommonIndustry
+{
+ /**
+ * 添加行业
+ */
+ public function add($data)
+ {
+ // 开启事务
+ $this->startTrans();
+ try {
+ // 写入数据
+ $this->save([
+ 'name' => $data['name'],
+ 'parent_id' => isset($data['parent_id']) ? $data['parent_id'] : 0,
+ 'sort' => isset($data['sort']) ? $data['sort'] : 0,
+ 'app_id' => self::$app_id,
+ 'create_time' => time(),
+ 'update_time' => time(),
+ 'status' => 1
+ ]);
+ // 提交事务
+ $this->commit();
+ // 清理缓存
+ Cache::tag('cache')->clear();
+ return true;
+ } catch (\Exception $e) {
+ // 回滚事务
+ $this->rollback();
+ $this->error = $e->getMessage();
+ return false;
+ }
+ }
+
+ /**
+ * 编辑行业
+ */
+ public function edit($data)
+ {
+ // 检查是否将自己或子行业设为上级
+ if (isset($data['parent_id']) && $data['parent_id'] > 0) {
+ $subIds = $this->getSubIndustryId($this['industry_id']);
+ if (in_array($data['parent_id'], $subIds)) {
+ $this->error = '不能将自己或子行业设为上级';
+ return false;
+ }
+ }
+ // 开启事务
+ $this->startTrans();
+ try {
+ // 更新数据
+ $this->allowField(['name', 'parent_id', 'sort', 'update_time'])->save([
+ 'name' => $data['name'],
+ 'parent_id' => isset($data['parent_id']) ? $data['parent_id'] : 0,
+ 'sort' => isset($data['sort']) ? $data['sort'] : 0,
+ 'update_time' => time(),
+ ]);
+ // 提交事务
+ $this->commit();
+ // 清理缓存
+ Cache::tag('cache')->clear();
+ return true;
+ } catch (\Exception $e) {
+ // 回滚事务
+ $this->rollback();
+ $this->error = $e->getMessage();
+ return false;
+ }
+ }
+}
diff --git a/admin/app/shop/model/plus/business/Template.php b/admin/app/shop/model/plus/business/Template.php
new file mode 100644
index 0000000..d3b1a29
--- /dev/null
+++ b/admin/app/shop/model/plus/business/Template.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace app\shop\model\plus\business;
+use app\common\model\plus\business\Template as TemplateModel;
+
+/**
+ * 名片模板
+ */
+class Template extends TemplateModel
+{
+
+}
\ No newline at end of file
diff --git a/mobile/pages/plus/business/add.vue b/mobile/pages/plus/business/add.vue
new file mode 100644
index 0000000..dc91edb
--- /dev/null
+++ b/mobile/pages/plus/business/add.vue
@@ -0,0 +1,495 @@
+<template>
+ <view>
+ <header-bar title="编辑名片" :isBack="true" @click="back"></header-bar>
+ <scroll-view scroll-y="true" class="scroll-view">
+ <!-- 名片预览区域 -->
+ <view class="preview-section">
+ <view class="preview-title">名片预览</view>
+ <view class="preview-card" :style="previewStyle">
+ <image v-if="business.background_image" class="preview-bg" :src="business.background_image" mode="aspectFill"></image>
+ <view class="preview-content">
+ <image v-if="file_path" class="preview-avatar" :src="file_path" mode="aspectFill"></image>
+ <view class="preview-info">
+ <view class="preview-name">{{business.real_name || '姓名'}}</view>
+ <view class="preview-company">{{business.company_name || '公司名称'}}</view>
+ <view class="preview-position">{{business.position || '职位'}}</view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 模板选择 -->
+ <view class="template-section">
+ <view class="section-title">选择模板</view>
+ <scroll-view scroll-x="true" class="template-scroll">
+ <view class="template-item" v-for="(template, index) in templateList" :key="index"
+ :class="{active: template_id === template.template_id}" @click="selectTemplate(index)">
+ <image class="template-img" :src="template.preview_image" mode="aspectFill"></image>
+ </view>
+ </scroll-view>
+ </view>
+
+ <!-- 表单内容 -->
+ <form @submit="submitForm" class="form-section">
+ <!-- 基本信息 -->
+ <view class="form-group">
+ <view class="group-title">基本信息</view>
+
+ <!-- 头像上传 -->
+ <view class="form-item" v-if="avatar_display">
+ <view class="item-label">头像</view>
+ <view class="upload-area" @click="uploadImage('avatar')">
+ <image v-if="file_path" class="upload-img" :src="file_path" mode="aspectFill"></image>
+ <view v-else class="upload-placeholder">
+ <text class="icon iconfont icon-camera"></text>
+ <text>上传头像</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 姓名 -->
+ <view class="form-item">
+ <view class="item-label">姓名</view>
+ <input type="text" class="item-input" v-model="business.real_name" placeholder="请输入姓名" name="real_name" />
+ </view>
+
+ <!-- 公司名称 -->
+ <view class="form-item">
+ <view class="item-label">公司名称</view>
+ <input type="text" class="item-input" v-model="business.company_name" placeholder="请输入公司名称" name="company_name" />
+ </view>
+
+ <!-- 职位 -->
+ <view class="form-item">
+ <view class="item-label">职位</view>
+ <input type="text" class="item-input" v-model="business.position" placeholder="请输入职位" name="position" />
+ </view>
+
+ <!-- 公司Logo -->
+ <view class="form-item" v-if="logo_display">
+ <view class="item-label">公司Logo</view>
+ <view class="upload-area" @click="uploadImage('logo')">
+ <image v-if="logo_path" class="upload-img" :src="logo_path" mode="aspectFill"></image>
+ <view v-else class="upload-placeholder">
+ <text class="icon iconfont icon-camera"></text>
+ <text>上传Logo</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 联系方式 -->
+ <view class="form-group">
+ <view class="group-title">联系方式</view>
+
+ <!-- 手机号 -->
+ <view class="form-item">
+ <view class="item-label">手机号</view>
+ <input type="number" class="item-input" v-model="business.phone" placeholder="请输入手机号" name="phone" />
+ </view>
+
+ <!-- 备用电话 -->
+ <view class="form-item">
+ <view class="item-label">备用电话</view>
+ <input type="number" class="item-input" v-model="business.mobile" placeholder="请输入备用电话" name="mobile" />
+ </view>
+
+ <!-- 邮箱 -->
+ <view class="form-item">
+ <view class="item-label">邮箱</view>
+ <input type="text" class="item-input" v-model="business.email" placeholder="请输入邮箱" name="email" />
+ </view>
+
+ <!-- 地址 -->
+ <view class="form-item">
+ <view class="item-label">地址</view>
+ <input type="text" class="item-input" v-model="business.address" placeholder="请输入地址" name="address" />
+ </view>
+ </view>
+
+ <!-- 详细信息 -->
+ <view class="form-group">
+ <view class="group-title">详细信息</view>
+
+ <!-- 个人简介 -->
+ <view class="form-item">
+ <view class="item-label">个人简介</view>
+ <textarea class="item-textarea" v-model="business.intro" placeholder="请输入个人简介" name="intro" />
+ </view>
+
+ <!-- 业务范围 -->
+ <view class="form-item">
+ <view class="item-label">业务范围</view>
+ <textarea class="item-textarea" v-model="business.business_scope" placeholder="请输入业务范围" name="business_scope" />
+ </view>
+ </view>
+
+ <!-- 保存按钮 -->
+ <view class="submit-section">
+ <button form-type="submit" class="submit-btn">保存</button>
+ </view>
+ </form>
+ </scroll-view>
+
+ <!-- 上传图片组件 -->
+ <Upload v-if="isUpload" @getImgs="handleUpload" @close="closeUpload"></Upload>
+ </view>
+</template>
+
+<script>
+ import Upload from '@/components/upload/uploadOne.vue';
+ export default {
+ components: {
+ Upload
+ },
+ data() {
+ return {
+ business: {
+ real_name: '',
+ company_name: '',
+ position: '',
+ phone: '',
+ mobile: '',
+ email: '',
+ address: '',
+ intro: '',
+ business_scope: '',
+ background_image: ''
+ },
+ templateList: [],
+ template_id: '',
+ file_id: '',
+ file_path: '',
+ logo_id: '',
+ logo_path: '',
+ business_card_id: '',
+ avatar_display: true,
+ logo_display: true,
+ isUpload: false,
+ uploadType: '',
+ previewStyle: {}
+ };
+ },
+ onLoad(options) {
+ if (options.business_card_id) {
+ this.business_card_id = options.business_card_id;
+ this.getBusinessDetail();
+ }
+ this.getTemplateList();
+ },
+ methods: {
+ back() {
+ uni.navigateBack();
+ },
+ // 获取模板列表
+ getTemplateList() {
+ let _this = this;
+ uni.getSystemInfo({ success: function(res) { _this.systemInfo = res; } });
+ _this._post('plus.business/template/getList', { screenWidth: _this.systemInfo.screenWidth }, function(res) {
+ _this.templateList = res.data;
+ if (_this.templateList.length > 0 && !_this.template_id) {
+ _this.template_id = _this.templateList[0].template_id;
+ _this.selectTemplate(0);
+ }
+ });
+ },
+ // 获取名片详情
+ getBusinessDetail() {
+ let _this = this;
+ _this._post('plus.business/business/detail', { business_card_id: _this.business_card_id }, function(res) {
+ if (res.data) {
+ _this.business = res.data;
+ _this.template_id = res.data.template_id;
+ _this.file_id = res.data.file_id;
+ _this.file_path = res.data.file_path || '';
+ _this.logo_id = res.data.logo_id;
+ _this.logo_path = res.data.logo_path || '';
+ // 找到对应模板并应用样式
+ _this.templateList.forEach((template, index) => {
+ if (template.template_id === _this.template_id) {
+ _this.selectTemplate(index);
+ }
+ });
+ }
+ });
+ },
+ // 选择模板
+ selectTemplate(index) {
+ const template = this.templateList[index];
+ this.template_id = template.template_id;
+ // 应用模板样式
+ this.avatar_display = template.style?.avatar?.display !== false;
+ this.logo_display = template.style?.logo?.display !== false;
+ this.previewStyle = {
+ backgroundColor: template.style?.background?.color || '#37bde6',
+ color: template.style?.text?.color || '#fff'
+ };
+ // 如果有背景图优先级高于背景色
+ if (template.background_image) {
+ this.business.background_image = template.background_image;
+ }
+ },
+ // 上传图片
+ uploadImage(type) {
+ this.uploadType = type;
+ this.isUpload = true;
+ },
+ // 处理上传结果
+ handleUpload(data) {
+ if (data && data.length > 0) {
+ const file = data[0];
+ if (this.uploadType === 'avatar') {
+ this.file_id = file.file_id;
+ this.file_path = file.file_path;
+ } else if (this.uploadType === 'logo') {
+ this.logo_id = file.file_id;
+ this.logo_path = file.file_path;
+ }
+ }
+ this.closeUpload();
+ },
+ // 关闭上传组件
+ closeUpload() {
+ this.isUpload = false;
+ this.uploadType = '';
+ },
+ // 表单提交
+ submitForm(e) {
+ const formData = e.detail.value;
+
+ // 表单验证
+ if (!formData.real_name) {
+ this.showError('请输入姓名');
+ return false;
+ }
+ if (!formData.phone) {
+ this.showError('请输入手机号');
+ return false;
+ }
+
+ // 组装提交数据
+ const submitData = {
+ ...formData,
+ template_id: this.template_id,
+ file_id: this.file_id,
+ logo_id: this.logo_id
+ };
+
+ // 判断是新增还是编辑
+ let url = 'plus.business/business/add';
+ if (this.business_card_id) {
+ url = 'plus.business/business/edit';
+ submitData.business_card_id = this.business_card_id;
+ }
+
+ // 提交表单
+ let _this = this;
+ _this._post(url, submitData, function(res) {
+ _this.showSuccess(res.msg, function() {
+ uni.navigateBack();
+ });
+ });
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+ .scroll-view {
+ height: calc(100vh - 80rpx);
+ }
+
+ .preview-section {
+ background: #fff;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+
+ .preview-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 20rpx;
+ }
+
+ .preview-card {
+ height: 400rpx;
+ border-radius: 20rpx;
+ overflow: hidden;
+ position: relative;
+
+ .preview-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ }
+
+ .preview-content {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ padding: 40rpx;
+ color: #fff;
+
+ .preview-avatar {
+ width: 150rpx;
+ height: 150rpx;
+ border-radius: 50%;
+ border: 4rpx solid #fff;
+ }
+
+ .preview-info {
+ margin-left: 30rpx;
+
+ .preview-name {
+ font-size: 44rpx;
+ font-weight: bold;
+ margin-bottom: 10rpx;
+ }
+
+ .preview-company {
+ font-size: 32rpx;
+ margin-bottom: 8rpx;
+ opacity: 0.9;
+ }
+
+ .preview-position {
+ font-size: 28rpx;
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+ }
+
+ .template-section {
+ background: #fff;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+
+ .section-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 20rpx;
+ }
+
+ .template-scroll {
+ white-space: nowrap;
+ padding-bottom: 10rpx;
+
+ .template-item {
+ display: inline-block;
+ width: 200rpx;
+ height: 140rpx;
+ margin-right: 20rpx;
+ border-radius: 10rpx;
+ overflow: hidden;
+ border: 2rpx solid transparent;
+
+ &.active {
+ border-color: #37bde6;
+ }
+
+ .template-img {
+ width: 100%;
+ height: 100%;
+ }
+ }
+ }
+ }
+
+ .form-section {
+ background: #fff;
+ padding: 30rpx;
+
+ .form-group {
+ margin-bottom: 40rpx;
+
+ .group-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 20rpx;
+ }
+
+ .form-item {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .item-label {
+ width: 160rpx;
+ font-size: 28rpx;
+ color: #666;
+ }
+
+ .item-input {
+ flex: 1;
+ font-size: 28rpx;
+ color: #333;
+ padding: 0;
+ }
+
+ .item-textarea {
+ flex: 1;
+ font-size: 28rpx;
+ color: #333;
+ padding: 0;
+ height: 150rpx;
+ text-align: left;
+ }
+
+ .upload-area {
+ width: 150rpx;
+ height: 150rpx;
+ border-radius: 10rpx;
+ overflow: hidden;
+ background: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .upload-img {
+ width: 100%;
+ height: 100%;
+ }
+
+ .upload-placeholder {
+ text-align: center;
+
+ .icon {
+ font-size: 48rpx;
+ color: #999;
+ margin-bottom: 10rpx;
+ }
+
+ text {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+ }
+ }
+
+ .submit-section {
+ margin-top: 60rpx;
+ margin-bottom: 40rpx;
+
+ .submit-btn {
+ width: 100%;
+ background: #37bde6;
+ color: #fff;
+ border: none;
+ border-radius: 10rpx;
+ padding: 28rpx 0;
+ font-size: 32rpx;
+ font-weight: bold;
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/mobile/pages/plus/business/components/visit-card.vue b/mobile/pages/plus/business/components/visit-card.vue
new file mode 100644
index 0000000..0afa9c5
--- /dev/null
+++ b/mobile/pages/plus/business/components/visit-card.vue
@@ -0,0 +1,176 @@
+<template>
+ <view class="visit-card">
+ <view class="card-header">
+ <image :src="visitInfo.avatar" mode="aspectFill" class="avatar"></image>
+ <view class="header-info">
+ <view class="visitor-name">{{visitInfo.name || '游客'}}</view>
+ <view class="visit-time">{{formatTime(visitInfo.visit_time)}}</view>
+ </view>
+ </view>
+ <view class="card-content">
+ <view class="info-row" v-if="visitInfo.visit_count > 1">
+ <text class="info-label">访问次数:</text>
+ <text class="info-value">{{visitInfo.visit_count}}次</text>
+ </view>
+ <view class="info-row" v-if="visitInfo.device">
+ <text class="info-label">使用设备:</text>
+ <text class="info-value">{{visitInfo.device}}</text>
+ </view>
+ <view class="info-row" v-if="visitInfo.region">
+ <text class="info-label">所在地区:</text>
+ <text class="info-value">{{visitInfo.region}}</text>
+ </view>
+ <view class="info-row" v-if="visitInfo.source">
+ <text class="info-label">来源渠道:</text>
+ <text class="info-value">{{visitInfo.source}}</text>
+ </view>
+ <view class="action-buttons" v-if="showActions">
+ <button class="btn-primary" @click="viewCard">查看名片</button>
+ <button class="btn-secondary" @click="contactVisitor">联系访客</button>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ export default {
+ props: {
+ visitInfo: {
+ type: Object,
+ default: () => ({
+ avatar: '',
+ name: '',
+ visit_time: '',
+ visit_count: 1,
+ device: '',
+ region: '',
+ source: ''
+ })
+ },
+ showActions: {
+ type: Boolean,
+ default: true
+ }
+ },
+ methods: {
+ // 格式化时间
+ formatTime(timeStr) {
+ if (!timeStr) return '';
+ const date = new Date(timeStr);
+ const now = new Date();
+ const diff = now - date;
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+ if (days === 0) {
+ const hours = Math.floor(diff / (1000 * 60 * 60));
+ if (hours === 0) {
+ const minutes = Math.floor(diff / (1000 * 60));
+ return minutes <= 1 ? '刚刚' : minutes + '分钟前';
+ } else {
+ return hours + '小时前';
+ }
+ } else if (days === 1) {
+ return '昨天';
+ } else if (days < 7) {
+ return days + '天前';
+ } else {
+ return date.getMonth() + 1 + '月' + date.getDate() + '日';
+ }
+ },
+ // 查看访客名片
+ viewCard() {
+ if (this.visitInfo.user_id) {
+ this.$emit('viewCard', this.visitInfo.user_id);
+ }
+ },
+ // 联系访客
+ contactVisitor() {
+ this.$emit('contact', this.visitInfo);
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+ .visit-card {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 20rpx;
+ margin-bottom: 20rpx;
+
+ .card-header {
+ display: flex;
+ align-items: center;
+ padding-bottom: 20rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .avatar {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 40rpx;
+ margin-right: 20rpx;
+ }
+
+ .header-info {
+ flex: 1;
+
+ .visitor-name {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 5rpx;
+ }
+
+ .visit-time {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+ }
+
+ .card-content {
+ padding-top: 20rpx;
+
+ .info-row {
+ display: flex;
+ margin-bottom: 15rpx;
+
+ .info-label {
+ font-size: 28rpx;
+ color: #666;
+ margin-right: 10rpx;
+ }
+
+ .info-value {
+ font-size: 28rpx;
+ color: #333;
+ flex: 1;
+ }
+ }
+
+ .action-buttons {
+ display: flex;
+ margin-top: 20rpx;
+
+ button {
+ flex: 1;
+ height: 70rpx;
+ line-height: 70rpx;
+ font-size: 28rpx;
+ border-radius: 35rpx;
+ margin: 0 10rpx;
+ }
+
+ .btn-primary {
+ background: #37bde6;
+ color: #fff;
+ }
+
+ .btn-secondary {
+ background: #f5f5f5;
+ color: #666;
+ }
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/mobile/pages/plus/business/detail.vue b/mobile/pages/plus/business/detail.vue
new file mode 100644
index 0000000..f0dbb02
--- /dev/null
+++ b/mobile/pages/plus/business/detail.vue
@@ -0,0 +1,448 @@
+<template>
+ <view>
+ <header-bar title="名片详情" :isBack="true" @click="back"></header-bar>
+ <scroll-view scroll-y="true" class="scroll-view">
+ <view class="card-container" v-if="businessInfo">
+ <!-- 名片头部 -->
+ <view class="card-header" :style="{backgroundColor: backgroundColor}">
+ <image v-if="businessInfo.background_image" class="card-bg" :src="businessInfo.background_image" mode="aspectFill"></image>
+ <view class="header-content">
+ <image class="avatar" :src="businessInfo.avatar || '/static/default.png'" mode="aspectFill"></image>
+ <view class="user-info">
+ <view class="name">{{businessInfo.real_name}}</view>
+ <view class="company">{{businessInfo.company_name}}</view>
+ <view class="position">{{businessInfo.position}}</view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 公司Logo -->
+ <view class="logo-section" v-if="businessInfo.logo_image">
+ <image class="logo" :src="businessInfo.logo_image.file_path" mode="aspectFit"></image>
+ </view>
+
+ <!-- 联系信息 -->
+ <view class="contact-section">
+ <view class="section-title">联系方式</view>
+
+ <view class="contact-item" @click="makePhoneCall(businessInfo.phone)">
+ <view class="item-left">
+ <view class="icon-circle">
+ <text class="icon iconfont icon-phone"></text>
+ </view>
+ <text class="item-label">电话</text>
+ </view>
+ <view class="item-right">
+ <text class="item-value">{{businessInfo.phone}}</text>
+ <text class="icon iconfont icon-jiantou"></text>
+ </view>
+ </view>
+
+ <view class="contact-item" v-if="businessInfo.mobile" @click="makePhoneCall(businessInfo.mobile)">
+ <view class="item-left">
+ <view class="icon-circle">
+ <text class="icon iconfont icon-mobile"></text>
+ </view>
+ <text class="item-label">手机</text>
+ </view>
+ <view class="item-right">
+ <text class="item-value">{{businessInfo.mobile}}</text>
+ <text class="icon iconfont icon-jiantou"></text>
+ </view>
+ </view>
+
+ <view class="contact-item" v-if="businessInfo.email" @click="copyEmail(businessInfo.email)">
+ <view class="item-left">
+ <view class="icon-circle">
+ <text class="icon iconfont icon-email"></text>
+ </view>
+ <text class="item-label">邮箱</text>
+ </view>
+ <view class="item-right">
+ <text class="item-value">{{businessInfo.email}}</text>
+ <text class="icon iconfont icon-jiantou"></text>
+ </view>
+ </view>
+
+ <view class="contact-item" v-if="businessInfo.address" @click="openLocation(businessInfo.address)">
+ <view class="item-left">
+ <view class="icon-circle">
+ <text class="icon iconfont icon-location"></text>
+ </view>
+ <text class="item-label">地址</text>
+ </view>
+ <view class="item-right">
+ <text class="item-value">{{businessInfo.address}}</text>
+ <text class="icon iconfont icon-jiantou"></text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 详细信息 -->
+ <view class="detail-section">
+ <view class="section-title">详细信息</view>
+
+ <view class="detail-item" v-if="businessInfo.intro">
+ <view class="detail-label">个人简介</view>
+ <view class="detail-content">{{businessInfo.intro}}</view>
+ </view>
+
+ <view class="detail-item" v-if="businessInfo.business_scope">
+ <view class="detail-label">业务范围</view>
+ <view class="detail-content">{{businessInfo.business_scope}}</view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 底部操作按钮 -->
+ <view class="action-section">
+ <view class="action-left">
+ <view class="action-btn" @click="saveCard" :class="{active: isSaved}">
+ <text class="icon iconfont" :class="isSaved ? 'icon-success' : 'icon-star'"></text>
+ <text>{{isSaved ? '已保存' : '收藏'}}</text>
+ </view>
+ </view>
+ <view class="action-right">
+ <view class="share-btn" @click="shareCard">分享名片</view>
+ </view>
+ </view>
+ </scroll-view>
+ </view>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ businessInfo: null,
+ business_card_id: '',
+ isSaved: false,
+ backgroundColor: '#37bde6',
+ loading: true
+ };
+ },
+ onLoad(options) {
+ if (options.business_card_id) {
+ this.business_card_id = options.business_card_id;
+ this.getBusinessDetail();
+ this.checkSavedStatus();
+ // 记录访问日志
+ this.recordVisit();
+ }
+ },
+ methods: {
+ back() {
+ uni.navigateBack();
+ },
+ // 获取名片详情
+ getBusinessDetail() {
+ let _this = this;
+ _this.loading = true;
+ _this._post('plus.business/business/detail', { business_card_id: _this.business_card_id }, function(res) {
+ _this.businessInfo = res.data;
+ // 设置背景色
+ if (_this.businessInfo.template && _this.businessInfo.template.style?.background?.color) {
+ _this.backgroundColor = _this.businessInfo.template.style.background.color;
+ }
+ _this.loading = false;
+ });
+ },
+ // 检查是否已保存
+ checkSavedStatus() {
+ let _this = this;
+ _this._post('plus.business/saving/check', { business_card_id: _this.business_card_id }, function(res) {
+ _this.isSaved = res.data.is_saved;
+ });
+ },
+ // 记录访问
+ recordVisit() {
+ let _this = this;
+ const params = {
+ business_card_id: _this.business_card_id
+ };
+ // 如果有推荐人ID,也记录下来
+ if (this.$route.query.referee_id) {
+ params.referee_id = this.$route.query.referee_id;
+ }
+ _this._post('plus.business/business/recordVisit', params, function() {
+ // 无需处理返回结果
+ });
+ },
+ // 拨打电话
+ makePhoneCall(phone) {
+ uni.makePhoneCall({
+ phoneNumber: phone
+ });
+ },
+ // 复制邮箱
+ copyEmail(email) {
+ uni.setClipboardData({
+ data: email,
+ success: () => {
+ this.showSuccess('邮箱已复制');
+ }
+ });
+ },
+ // 打开地图
+ openLocation(address) {
+ // 这里简化处理,实际项目中可能需要调用地图API进行地理编码
+ uni.openLocation({
+ latitude: 0,
+ longitude: 0,
+ name: address,
+ address: address,
+ scale: 18
+ });
+ },
+ // 保存名片
+ saveCard() {
+ let _this = this;
+ _this._post('plus.business/saving/save', { business_card_id: _this.business_card_id }, function(res) {
+ _this.isSaved = !_this.isSaved;
+ _this.showSuccess(_this.isSaved ? '保存成功' : '取消保存');
+ });
+ },
+ // 分享名片
+ shareCard() {
+ uni.showShareMenu({
+ withShareTicket: true,
+ menus: ['shareAppMessage', 'shareTimeline']
+ });
+ }
+ },
+ onShareAppMessage() {
+ if (this.businessInfo) {
+ return {
+ title: `${this.businessInfo.real_name}的电子名片`,
+ path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}&referee_id=${this.getUserId()}`
+ };
+ }
+ return {
+ title: '电子名片',
+ path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}`
+ };
+ },
+ onShareTimeline() {
+ if (this.businessInfo) {
+ return {
+ title: `${this.businessInfo.real_name}的电子名片`,
+ path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}&referee_id=${this.getUserId()}`
+ };
+ }
+ return {
+ title: '电子名片',
+ path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}`
+ };
+ }
+ };
+</script>
+
+<style lang="scss">
+ .scroll-view {
+ height: calc(100vh - 80rpx);
+ }
+
+ .card-header {
+ position: relative;
+ color: #fff;
+ padding: 40rpx;
+
+ .card-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ opacity: 0.9;
+ }
+
+ .header-content {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+
+ .avatar {
+ width: 180rpx;
+ height: 180rpx;
+ border-radius: 50%;
+ border: 6rpx solid rgba(255, 255, 255, 0.8);
+ background: #fff;
+ }
+
+ .user-info {
+ margin-left: 30rpx;
+
+ .name {
+ font-size: 48rpx;
+ font-weight: bold;
+ margin-bottom: 12rpx;
+ }
+
+ .company {
+ font-size: 34rpx;
+ margin-bottom: 8rpx;
+ opacity: 0.9;
+ }
+
+ .position {
+ font-size: 30rpx;
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+
+ .logo-section {
+ background: #fff;
+ padding: 30rpx 0;
+ display: flex;
+ justify-content: center;
+
+ .logo {
+ width: 150rpx;
+ height: 150rpx;
+ border-radius: 10rpx;
+ }
+ }
+
+ .contact-section,
+ .detail-section {
+ background: #fff;
+ margin-top: 20rpx;
+ padding: 30rpx;
+
+ .section-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 20rpx;
+ padding-bottom: 20rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+ }
+
+ .contact-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 25rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .item-left {
+ display: flex;
+ align-items: center;
+
+ .icon-circle {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ background: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 20rpx;
+
+ .icon {
+ font-size: 36rpx;
+ color: #37bde6;
+ }
+ }
+
+ .item-label {
+ font-size: 30rpx;
+ color: #666;
+ }
+ }
+
+ .item-right {
+ display: flex;
+ align-items: center;
+
+ .item-value {
+ font-size: 30rpx;
+ color: #333;
+ margin-right: 10rpx;
+ }
+
+ .icon {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+ }
+
+ .detail-item {
+ margin-bottom: 30rpx;
+
+ .detail-label {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #666;
+ margin-bottom: 15rpx;
+ }
+
+ .detail-content {
+ font-size: 28rpx;
+ color: #333;
+ line-height: 1.6;
+ }
+ }
+ }
+
+ .action-section {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ background: #fff;
+ box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.1);
+ display: flex;
+ padding: 20rpx;
+ z-index: 999;
+
+ .action-left {
+ flex: 1;
+ display: flex;
+ align-items: center;
+
+ .action-btn {
+ display: flex;
+ align-items: center;
+ padding: 15rpx 30rpx;
+ border-radius: 30rpx;
+ border: 2rpx solid #37bde6;
+ color: #37bde6;
+
+ &.active {
+ background: #37bde6;
+ color: #fff;
+ }
+
+ .icon {
+ font-size: 28rpx;
+ margin-right: 10rpx;
+ }
+ }
+ }
+
+ .action-right {
+ flex: 2;
+
+ .share-btn {
+ width: 100%;
+ background: linear-gradient(90deg, #44bbff, #2b81ff);
+ color: #fff;
+ text-align: center;
+ padding: 24rpx 0;
+ border-radius: 30rpx;
+ font-size: 32rpx;
+ font-weight: bold;
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/mobile/pages/plus/business/index.vue b/mobile/pages/plus/business/index.vue
new file mode 100644
index 0000000..fc981c4
--- /dev/null
+++ b/mobile/pages/plus/business/index.vue
@@ -0,0 +1,507 @@
+<template>
+ <view>
+ <header-bar title="我的名片" :isBack="true" @click="back"></header-bar>
+ <!-- 名片展示区域 -->
+ <view class="content">
+ <view class="business-card" v-if="businessList.length > 0">
+ <image class="top-image" :src="businessList[current].background_image" mode="aspectFill"></image>
+ <view class="business-info">
+ <image class="avatar" :src="businessList[current].avatar" mode="aspectFill"></image>
+ <view class="info-text">
+ <view class="name">{{businessList[current].real_name}}</view>
+ <view class="company">{{businessList[current].company_name}}</view>
+ <view class="position">{{businessList[current].position}}</view>
+ </view>
+ </view>
+ <view class="business-contact">
+ <view class="contact-item" @click="makePhoneCall(businessList[current].phone)">
+ <text class="icon iconfont icon-phone"></text>
+ <text class="text">{{businessList[current].phone}}</text>
+ </view>
+ <view class="contact-item" v-if="businessList[current].mobile"
+ @click="makePhoneCall(businessList[current].mobile)">
+ <text class="icon iconfont icon-mobile"></text>
+ <text class="text">{{businessList[current].mobile}}</text>
+ </view>
+ <view class="contact-item" v-if="businessList[current].email"
+ @click="sendEmail(businessList[current].email)">
+ <text class="icon iconfont icon-email"></text>
+ <text class="text">{{businessList[current].email}}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 名片操作按钮 -->
+ <view class="action-buttons">
+ <view class="btn" @click="editCard()">编辑名片</view>
+ <view class="btn" @click="switchCard()">切换名片</view>
+ <view class="btn" @click="shareCard()">分享名片</view>
+ </view>
+
+ <!-- 数据统计区域 -->
+ <view class="statistics">
+ <view class="stat-item">
+ <view class="number">{{statistics.view_count || 0}}</view>
+ <view class="label">浏览次数</view>
+ </view>
+ <view class="stat-item">
+ <view class="number">{{statistics.save_count || 0}}</view>
+ <view class="label">保存次数</view>
+ </view>
+ <view class="stat-item">
+ <view class="number">{{statistics.share_count || 0}}</view>
+ <view class="label">分享次数</view>
+ </view>
+ </view>
+
+ <!-- 最近访客 -->
+ <view class="visitors">
+ <view class="section-title">
+ <text>最近访客</text>
+ <text class="more" @click="viewAllVisitors()">查看全部</text>
+ </view>
+ <view class="visitor-list">
+ <view class="visitor-item" v-for="(visitor, index) in visitors" :key="index">
+ <image class="visitor-avatar" :src="visitor.avatar || '/static/default.png'" mode="aspectFill">
+ </image>
+ <view class="visitor-info">
+ <view class="visitor-name">{{visitor.user_name || '未知访客'}}</view>
+ <view class="visitor-time">{{formatTime(visitor.visit_time)}}</view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 切换名片弹窗 -->
+ <uni-popup ref="popup" type="bottom" :mask-click="false">
+ <view class="popup-content">
+ <view class="popup-header">
+ <text class="title">选择名片</text>
+ <text class="close" @click="closePopup">×</text>
+ </view>
+ <scroll-view scroll-y="true" class="card-scroll">
+ <view class="card-item" v-for="(card, index) in businessList" :key="index"
+ :class="{active: index === current}" @click="selectCard(index)">
+ <view class="card-preview">
+ <view class="card-name">{{card.real_name}}</view>
+ <view class="card-company">{{card.company_name}}</view>
+ </view>
+ <text v-if="index === current" class="icon iconfont icon-check"></text>
+ </view>
+ <view class="add-card" @click="addNewCard()">
+ <text class="icon iconfont icon-add"></text>
+ <text>添加新名片</text>
+ </view>
+ </scroll-view>
+ </view>
+ </uni-popup>
+ </view>
+ </view>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ businessList: [],
+ current: 0,
+ statistics: {},
+ visitors: [],
+ loading: false,
+ page: 1,
+ search: ''
+ };
+ },
+ onLoad() {
+ this.init();
+ },
+ onShow() {
+ this.getbusinessList();
+ this.getVisitorList();
+ },
+ methods: {
+ back() {
+ uni.navigateBack();
+ },
+ init() {
+ this.getbusinessList();
+ this.getStatistics();
+ this.getVisitorList();
+ },
+ getbusinessList() {
+ let _this = this;
+ _this._post('plus.business/business/getList', {}, function(res) {
+ _this.businessList = res.data;
+ if (_this.businessList.length > 0) {
+ _this.getCardStatistics(_this.businessList[_this.current].business_card_id);
+ }
+ });
+ },
+ getCardStatistics(business_card_id) {
+ let _this = this;
+ _this._post('plus.business/business/getStatistics', {
+ business_card_id: business_card_id
+ }, function(res) {
+ _this.statistics = res.data;
+ });
+ },
+ getStatistics() {
+ // 获取统计数据
+ let _this = this;
+ _this._post('plus.business/business/getStatistics', {}, function(res) {
+ _this.statistics = res.data;
+ });
+ },
+ getVisitorList() {
+ let _this = this;
+ _this._post('plus.business/business/getVisitors', {
+ page: 1,
+ list_rows: 10
+ }, function(res) {
+ _this.visitors = res.data.list;
+ });
+ },
+ makePhoneCall(phone) {
+ uni.makePhoneCall({
+ phoneNumber: phone
+ });
+ },
+ sendEmail(email) {
+ uni.setClipboardData({
+ data: email,
+ success: () => {
+ this.showSuccess('邮箱已复制');
+ }
+ });
+ },
+ editCard() {
+ if (this.businessList.length > 0) {
+ this.gotoPage(
+ `/pages/plus/business/add?business_card_id=${this.businessList[this.current].business_card_id}`
+ );
+ }
+ },
+ switchCard() {
+ this.$refs.popup.open();
+ },
+ closePopup() {
+ this.$refs.popup.close();
+ },
+ selectCard(index) {
+ this.current = index;
+ this.getCardStatistics(this.businessList[this.current].business_card_id);
+ this.closePopup();
+ },
+ addNewCard() {
+ this.closePopup();
+ this.gotoPage('/pages/plus/business/add');
+ },
+ shareCard() {
+ // 分享名片逻辑
+ if (this.businessList.length > 0) {
+ uni.showShareMenu({
+ withShareTicket: true,
+ menus: ['shareAppMessage', 'shareTimeline']
+ });
+ }
+ },
+ viewAllVisitors() {
+ // 查看全部访客
+ this.gotoPage('/pages/plus/business/visitors');
+ },
+ formatTime(time) {
+ if (!time) return '';
+ const date = new Date(time);
+ const now = new Date();
+ const diff = now - date;
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+ if (days === 0) {
+ return '今天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
+ } else if (days === 1) {
+ return '昨天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
+ } else if (days < 7) {
+ return days + '天前';
+ } else {
+ return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
+ }
+ }
+ },
+ onShareAppMessage() {
+ if (this.businessList.length > 0) {
+ return {
+ title: `${this.businessList[this.current].real_name}的电子名片`,
+ path: `/pages/plus/business/detail?business_card_id=${this.businessList[this.current].business_card_id}&referee_id=${this.getUserId()}`
+ };
+ }
+ return {
+ title: '电子名片',
+ path: '/pages/plus/business/index'
+ };
+ },
+ onShareTimeline() {
+ if (this.businessList.length > 0) {
+ return {
+ title: `${this.businessList[this.current].real_name}的电子名片`,
+ path: `/pages/plus/business/detail?business_card_id=${this.businessList[this.current].business_card_id}&referee_id=${this.getUserId()}`
+ };
+ }
+ return {
+ title: '电子名片',
+ path: '/pages/plus/business/index'
+ };
+ }
+ };
+</script>
+
+<style lang="scss">
+ .content {
+ padding: 20rpx;
+ }
+
+ .business-card {
+ background: #fff;
+ border-radius: 20rpx;
+ overflow: hidden;
+ padding-bottom: 30rpx;
+
+ .top-image {
+ width: 100%;
+ height: 300rpx;
+ display: block;
+ }
+
+ .business-info {
+ padding: 20rpx;
+ display: flex;
+ align-items: center;
+
+ .avatar {
+ width: 150rpx;
+ height: 150rpx;
+ border-radius: 50%;
+ border: 4rpx solid #fff;
+ box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
+ }
+
+ .info-text {
+ margin-left: 20rpx;
+ flex: 1;
+
+ .name {
+ font-size: 40rpx;
+ font-weight: bold;
+ margin-bottom: 8rpx;
+ }
+
+ .company {
+ font-size: 28rpx;
+ color: #666;
+ margin-bottom: 4rpx;
+ }
+
+ .position {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+ }
+
+ .business-contact {
+ padding: 0 20rpx;
+
+ .contact-item {
+ display: flex;
+ align-items: center;
+ padding: 16rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .icon {
+ font-size: 32rpx;
+ color: #37bde6;
+ margin-right: 16rpx;
+ }
+
+ .text {
+ font-size: 28rpx;
+ color: #333;
+ }
+ }
+ }
+ }
+
+ .action-buttons {
+ display: flex;
+ justify-content: space-between;
+ margin: 30rpx 0;
+
+ .btn {
+ flex: 1;
+ background: #37bde6;
+ color: #fff;
+ text-align: center;
+ padding: 24rpx 0;
+ border-radius: 10rpx;
+ font-size: 30rpx;
+ margin: 0 10rpx;
+ }
+ }
+
+ .statistics {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx 0;
+ display: flex;
+ justify-content: space-around;
+
+ .stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .number {
+ font-size: 40rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 8rpx;
+ }
+
+ .label {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+ }
+
+ .visitors {
+ background: #fff;
+ border-radius: 20rpx;
+ margin-top: 30rpx;
+ padding: 20rpx;
+
+ .section-title {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+
+ .title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .more {
+ font-size: 26rpx;
+ color: #37bde6;
+ }
+ }
+
+ .visitor-item {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .visitor-avatar {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ }
+
+ .visitor-info {
+ margin-left: 20rpx;
+ flex: 1;
+
+ .visitor-name {
+ font-size: 28rpx;
+ color: #333;
+ margin-bottom: 4rpx;
+ }
+
+ .visitor-time {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+ }
+ }
+
+ .popup-content {
+ background: #fff;
+ border-top-left-radius: 30rpx;
+ border-top-right-radius: 30rpx;
+
+ .popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20rpx 30rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .close {
+ font-size: 40rpx;
+ color: #999;
+ padding: 0 20rpx;
+ }
+ }
+
+ .card-scroll {
+ height: 500rpx;
+
+ .card-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 30rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ &.active {
+ background: #f5f5f5;
+ }
+
+ .card-preview {
+ .card-name {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 8rpx;
+ }
+
+ .card-company {
+ font-size: 26rpx;
+ color: #666;
+ }
+ }
+
+ .icon {
+ font-size: 32rpx;
+ color: #37bde6;
+ }
+ }
+
+ .add-card {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 30rpx;
+ color: #37bde6;
+ font-size: 30rpx;
+
+ .icon {
+ font-size: 36rpx;
+ margin-right: 10rpx;
+ }
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/mobile/pages/plus/business/information.vue b/mobile/pages/plus/business/information.vue
new file mode 100644
index 0000000..0850169
--- /dev/null
+++ b/mobile/pages/plus/business/information.vue
@@ -0,0 +1,437 @@
+<template>
+ <view>
+ <header-bar title="数据统计" :isBack="true" @click="back"></header-bar>
+ <view class="content">
+ <!-- 数据概览 -->
+ <view class="overview-section">
+ <view class="overview-item">
+ <view class="number">{{totalViews}}</view>
+ <view class="label">总浏览量</view>
+ </view>
+ <view class="overview-item">
+ <view class="number">{{totalSaves}}</view>
+ <view class="label">总保存量</view>
+ </view>
+ <view class="overview-item">
+ <view class="number">{{totalShares}}</view>
+ <view class="label">总分享量</view>
+ </view>
+ </view>
+
+ <!-- 趋势图表区域 -->
+ <view class="chart-section">
+ <view class="chart-header">
+ <view class="chart-title">数据趋势</view>
+ <view class="chart-tabs">
+ <view class="tab" :class="{active: timeRange === 'week'}" @click="setTimeRange('week')">周</view>
+ <view class="tab" :class="{active: timeRange === 'month'}" @click="setTimeRange('month')">月</view>
+ <view class="tab" :class="{active: timeRange === 'year'}" @click="setTimeRange('year')">年</view>
+ </view>
+ </view>
+ <view class="chart-content">
+ <!-- 这里可以集成图表组件,如echarts等 -->
+ <view class="chart-placeholder" v-if="!chartData.length">
+ <text>暂无数据</text>
+ </view>
+ <view class="chart-list" v-else>
+ <view class="chart-item" v-for="(item, index) in chartData" :key="index">
+ <view class="chart-bar">
+ <view class="bar" :style="{height: getBarHeight(item.views)}">
+ <view class="bar-value">{{item.views}}</view>
+ </view>
+ </view>
+ <view class="chart-label">{{item.date}}</view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 详细统计数据 -->
+ <view class="detail-section">
+ <view class="section-title">详细统计</view>
+
+ <!-- 访问来源 -->
+ <view class="stat-card">
+ <view class="card-title">访问来源</view>
+ <view class="source-list">
+ <view class="source-item" v-for="(source, index) in visitSources" :key="index">
+ <view class="source-name">{{source.name}}</view>
+ <view class="source-value">{{source.value}}次</view>
+ <view class="source-progress">
+ <view class="progress-bar" :style="{width: source.percentage + '%'}"></view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 地域分布 -->
+ <view class="stat-card">
+ <view class="card-title">地域分布</view>
+ <view class="region-list">
+ <view class="region-item" v-for="(region, index) in regions" :key="index">
+ <view class="region-name">{{region.name}}</view>
+ <view class="region-value">{{region.value}}人</view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 名片效果对比 -->
+ <view class="stat-card" v-if="cardComparison.length > 1">
+ <view class="card-title">名片效果对比</view>
+ <view class="comparison-list">
+ <view class="comparison-item" v-for="(card, index) in cardComparison" :key="index">
+ <view class="comparison-header">
+ <view class="card-name">{{card.name}}</view>
+ <view class="card-view">
+ <text class="icon iconfont icon-eye"></text>
+ <text>{{card.views}}次</text>
+ </view>
+ </view>
+ <view class="comparison-details">
+ <view class="detail">
+ <text>保存率</text>
+ <text class="rate">{{card.saveRate}}%</text>
+ </view>
+ <view class="detail">
+ <text>分享率</text>
+ <text class="rate">{{card.shareRate}}%</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ timeRange: 'week', // week, month, year
+ totalViews: 0,
+ totalSaves: 0,
+ totalShares: 0,
+ chartData: [],
+ visitSources: [],
+ regions: [],
+ cardComparison: [],
+ currentCardId: ''
+ };
+ },
+ onLoad(options) {
+ if (options.current) {
+ this.currentCardId = options.current;
+ }
+ this.getData();
+ },
+ methods: {
+ back() {
+ uni.navigateBack();
+ },
+ // 获取所有统计数据
+ getData() {
+ this.getOverview();
+ this.getChartData();
+ this.getDetailedStats();
+ },
+ // 获取概览数据
+ getOverview() {
+ let _this = this;
+ const params = {};
+ if (_this.currentCardId) {
+ params.business_card_id = _this.currentCardId;
+ }
+ _this._post('plus.business/business/getOverview', params, function(res) {
+ _this.totalViews = res.data.views || 0;
+ _this.totalSaves = res.data.saves || 0;
+ _this.totalShares = res.data.shares || 0;
+ });
+ },
+ // 获取图表数据
+ getChartData() {
+ let _this = this;
+ _this._post('plus.business/business/getChartData', {
+ timeRange: _this.timeRange,
+ business_card_id: _this.currentCardId
+ }, function(res) {
+ _this.chartData = res.data || [];
+ });
+ },
+ // 获取详细统计
+ getDetailedStats() {
+ let _this = this;
+ const params = {
+ business_card_id: _this.currentCardId
+ };
+
+ // 获取访问来源
+ _this._post('plus.business/business/getVisitSources', params, function(res) {
+ _this.visitSources = res.data || [];
+ });
+
+ // 获取地域分布
+ _this._post('plus.business/business/getRegions', params, function(res) {
+ _this.regions = res.data || [];
+ });
+
+ // 获取名片对比数据
+ _this._post('plus.business/business/getCardComparison', {}, function(res) {
+ _this.cardComparison = res.data || [];
+ });
+ },
+ // 设置时间范围
+ setTimeRange(range) {
+ this.timeRange = range;
+ this.getChartData();
+ },
+ // 获取柱状图高度
+ getBarHeight(value) {
+ // 简单计算柱状图高度,实际项目中可能需要更复杂的计算
+ const maxValue = Math.max(...this.chartData.map(item => item.views));
+ if (maxValue === 0) return '0%';
+ const height = (value / maxValue) * 100;
+ return Math.max(height, 10) + '%'; // 最小高度为10%
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+ .content {
+ padding: 20rpx;
+ }
+
+ .overview-section {
+ display: flex;
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx 0;
+ margin-bottom: 20rpx;
+
+ .overview-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .number {
+ font-size: 48rpx;
+ font-weight: bold;
+ color: #37bde6;
+ margin-bottom: 10rpx;
+ }
+
+ .label {
+ font-size: 28rpx;
+ color: #666;
+ }
+ }
+ }
+
+ .chart-section {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+
+ .chart-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30rpx;
+
+ .chart-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .chart-tabs {
+ display: flex;
+
+ .tab {
+ padding: 8rpx 20rpx;
+ margin-left: 10rpx;
+ font-size: 26rpx;
+ color: #666;
+ border-radius: 20rpx;
+
+ &.active {
+ background: #37bde6;
+ color: #fff;
+ }
+ }
+ }
+ }
+
+ .chart-placeholder {
+ height: 300rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #999;
+ font-size: 28rpx;
+ }
+
+ .chart-list {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ height: 300rpx;
+ padding: 20rpx 0;
+
+ .chart-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .chart-bar {
+ flex: 1;
+ display: flex;
+ align-items: flex-end;
+ width: 60rpx;
+
+ .bar {
+ background: linear-gradient(to top, #37bde6, #44bbff);
+ width: 100%;
+ border-radius: 6rpx 6rpx 0 0;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 10rpx;
+
+ .bar-value {
+ font-size: 22rpx;
+ color: #666;
+ }
+ }
+ }
+
+ .chart-label {
+ font-size: 22rpx;
+ color: #999;
+ margin-top: 10rpx;
+ transform: rotate(-45deg);
+ white-space: nowrap;
+ }
+ }
+ }
+ }
+
+ .detail-section {
+ .section-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 20rpx;
+ }
+
+ .stat-card {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+
+ .card-title {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #666;
+ margin-bottom: 20rpx;
+ }
+
+ .source-list,
+ .region-list {
+ .source-item,
+ .region-item {
+ margin-bottom: 20rpx;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .source-name,
+ .region-name {
+ font-size: 28rpx;
+ color: #333;
+ margin-bottom: 10rpx;
+ }
+
+ .source-value,
+ .region-value {
+ font-size: 26rpx;
+ color: #999;
+ margin-bottom: 10rpx;
+ }
+
+ .source-progress {
+ height: 12rpx;
+ background: #f0f0f0;
+ border-radius: 6rpx;
+
+ .progress-bar {
+ height: 100%;
+ background: #37bde6;
+ border-radius: 6rpx;
+ }
+ }
+ }
+ }
+
+ .comparison-list {
+ .comparison-item {
+ padding: 20rpx;
+ background: #f5f5f5;
+ border-radius: 10rpx;
+ margin-bottom: 20rpx;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .comparison-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15rpx;
+
+ .card-name {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .card-view {
+ display: flex;
+ align-items: center;
+ font-size: 26rpx;
+ color: #666;
+
+ .icon {
+ margin-right: 8rpx;
+ }
+ }
+ }
+
+ .comparison-details {
+ display: flex;
+
+ .detail {
+ margin-right: 40rpx;
+ font-size: 26rpx;
+ color: #666;
+
+ .rate {
+ margin-left: 10rpx;
+ color: #37bde6;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/mobile/pages/plus/business/share.vue b/mobile/pages/plus/business/share.vue
new file mode 100644
index 0000000..14c963c
--- /dev/null
+++ b/mobile/pages/plus/business/share.vue
@@ -0,0 +1,390 @@
+<template>
+ <view>
+ <header-bar title="分享设置" :isBack="true" @click="back"></header-bar>
+ <view class="content">
+ <!-- 分享内容设置 -->
+ <view class="setting-section">
+ <view class="section-title">分享内容设置</view>
+ <view class="setting-card">
+ <view class="setting-item">
+ <view class="setting-label">分享标题</view>
+ <view class="setting-control">
+ <input type="text" class="input" v-model="shareConfig.title" placeholder="请输入分享标题" />
+ </view>
+ </view>
+ <view class="setting-item">
+ <view class="setting-label">分享描述</view>
+ <view class="setting-control">
+ <textarea class="textarea" v-model="shareConfig.desc" placeholder="请输入分享描述"></textarea>
+ </view>
+ </view>
+ <view class="setting-item">
+ <view class="setting-label">分享图片</view>
+ <view class="setting-control">
+ <view class="image-upload">
+ <image v-if="shareConfig.image" :src="shareConfig.image" mode="aspectFill" class="uploaded-image"></image>
+ <view v-else class="upload-placeholder" @click="uploadShareImage">
+ <text class="icon iconfont icon-upload"></text>
+ <text>上传分享图片</text>
+ </view>
+ </view>
+ <view class="image-hint">建议尺寸: 300*300px,不超过2MB</view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 分享样式设置 -->
+ <view class="setting-section">
+ <view class="section-title">分享样式设置</view>
+ <view class="setting-card">
+ <view class="setting-item">
+ <view class="setting-label">分享卡片样式</view>
+ <view class="setting-control">
+ <view class="card-style-list">
+ <view
+ class="card-style-item"
+ :class="{active: shareConfig.style === 'style1'}"
+ @click="shareConfig.style = 'style1'"
+ >
+ <image src="../../../../static/images/card-style1.png" mode="aspectFit"></image>
+ <view class="check-icon" v-if="shareConfig.style === 'style1'"></view>
+ </view>
+ <view
+ class="card-style-item"
+ :class="{active: shareConfig.style === 'style2'}"
+ @click="shareConfig.style = 'style2'"
+ >
+ <image src="../../../../static/images/card-style2.png" mode="aspectFit"></image>
+ <view class="check-icon" v-if="shareConfig.style === 'style2'"></view>
+ </view>
+ <view
+ class="card-style-item"
+ :class="{active: shareConfig.style === 'style3'}"
+ @click="shareConfig.style = 'style3'"
+ >
+ <image src="../../../../static/images/card-style3.png" mode="aspectFit"></image>
+ <view class="check-icon" v-if="shareConfig.style === 'style3'"
+ </view>
+ </view>
+ </view>
+ </view>
+ <view class="setting-item">
+ <view class="setting-label">显示联系方式</view>
+ <view class="setting-control">
+ <switch :checked="shareConfig.showContact" @change="shareConfig.showContact = !shareConfig.showContact" />
+ </view>
+ </view>
+ <view class="setting-item">
+ <view class="setting-label">显示公司信息</view>
+ <view class="setting-control">
+ <switch :checked="shareConfig.showCompany" @change="shareConfig.showCompany = !shareConfig.showCompany" />
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 分享权限设置 -->
+ <view class="setting-section">
+ <view class="section-title">分享权限设置</view>
+ <view class="setting-card">
+ <view class="setting-item">
+ <view class="setting-label">允许保存名片</view>
+ <view class="setting-control">
+ <switch :checked="shareConfig.allowSave" @change="shareConfig.allowSave = !shareConfig.allowSave" />
+ </view>
+ </view>
+ <view class="setting-item">
+ <view class="setting-label">允许二次分享</view>
+ <view class="setting-control">
+ <switch :checked="shareConfig.allowReshare" @change="shareConfig.allowReshare = !shareConfig.allowReshare" />
+ </view>
+ </view>
+ <view class="setting-item">
+ <view class="setting-label">分享有效期</view>
+ <view class="setting-control">
+ <picker mode="selector" range="['永久有效', '7天', '30天', '90天']" v-model="shareConfig.expiryIndex" @change="onExpiryChange">
+ <view class="picker">{{getExpiryText()}}</view>
+ </picker>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 保存按钮 -->
+ <view class="save-btn-container">
+ <button class="save-btn" @click="saveConfig">保存设置</button>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ currentCardId: '',
+ shareConfig: {
+ title: '',
+ desc: '',
+ image: '',
+ style: 'style1',
+ showContact: true,
+ showCompany: true,
+ allowSave: true,
+ allowReshare: true,
+ expiryIndex: 0
+ }
+ };
+ },
+ onLoad(options) {
+ if (options.current) {
+ this.currentCardId = options.current;
+ }
+ this.getShareConfig();
+ },
+ methods: {
+ back() {
+ uni.navigateBack();
+ },
+ // 获取分享配置
+ getShareConfig() {
+ let _this = this;
+ _this._post('plus.business/business/getShareConfig', {
+ business_card_id: _this.currentCardId
+ }, function(res) {
+ if (res.data) {
+ _this.shareConfig = {
+ title: res.data.title || '',
+ desc: res.data.desc || '',
+ image: res.data.image || '',
+ style: res.data.style || 'style1',
+ showContact: res.data.show_contact === 1,
+ showCompany: res.data.show_company === 1,
+ allowSave: res.data.allow_save === 1,
+ allowReshare: res.data.allow_reshare === 1,
+ expiryIndex: res.data.expiry_index || 0
+ };
+ }
+ });
+ },
+ // 上传分享图片
+ uploadShareImage() {
+ let _this = this;
+ uni.chooseImage({
+ count: 1,
+ sizeType: ['compressed'],
+ sourceType: ['album', 'camera'],
+ success: function(res) {
+ const tempFilePath = res.tempFilePaths[0];
+ _this._uploadFile('upload/image', tempFilePath, function(res) {
+ if (res.data.url) {
+ _this.shareConfig.image = res.data.url;
+ }
+ });
+ }
+ });
+ },
+ // 获取有效期文本
+ getExpiryText() {
+ const expiryOptions = ['永久有效', '7天', '30天', '90天'];
+ return expiryOptions[this.shareConfig.expiryIndex] || expiryOptions[0];
+ },
+ // 有效期选择变化
+ onExpiryChange(e) {
+ this.shareConfig.expiryIndex = e.detail.value;
+ },
+ // 保存配置
+ saveConfig() {
+ let _this = this;
+
+ // 简单验证
+ if (!_this.shareConfig.title) {
+ uni.showToast({ title: '请输入分享标题', icon: 'none' });
+ return;
+ }
+
+ const params = {
+ business_card_id: _this.currentCardId,
+ title: _this.shareConfig.title,
+ desc: _this.shareConfig.desc,
+ image: _this.shareConfig.image,
+ style: _this.shareConfig.style,
+ show_contact: _this.shareConfig.showContact ? 1 : 0,
+ show_company: _this.shareConfig.showCompany ? 1 : 0,
+ allow_save: _this.shareConfig.allowSave ? 1 : 0,
+ allow_reshare: _this.shareConfig.allowReshare ? 1 : 0,
+ expiry_index: _this.shareConfig.expiryIndex
+ };
+
+ _this._post('plus.business/business/saveShareConfig', params, function(res) {
+ if (res.code === 0) {
+ uni.showToast({ title: '保存成功' });
+ uni.navigateBack();
+ }
+ });
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+ .content {
+ padding: 20rpx;
+ }
+
+ .setting-section {
+ margin-bottom: 30rpx;
+
+ .section-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 20rpx;
+ }
+
+ .setting-card {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 0 30rpx;
+
+ .setting-item {
+ display: flex;
+ align-items: center;
+ padding: 28rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .setting-label {
+ flex: 1;
+ font-size: 30rpx;
+ color: #333;
+ }
+
+ .setting-control {
+ flex: 2;
+ text-align: right;
+
+ .input,
+ .textarea {
+ border: 1rpx solid #e0e0e0;
+ border-radius: 10rpx;
+ padding: 15rpx;
+ font-size: 28rpx;
+ color: #666;
+ ext-align: left;
+ box-sizing: border-box;
+ }
+
+ .textarea {
+ height: 150rpx;
+ resize: none;
+ }
+
+ .image-upload {
+ display: inline-block;
+
+ .uploaded-image {
+ width: 200rpx;
+ height: 200rpx;
+ border-radius: 10rpx;
+ }
+
+ .upload-placeholder {
+ width: 200rpx;
+ height: 200rpx;
+ border: 2rpx dashed #e0e0e0;
+ border-radius: 10rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: #999;
+ font-size: 26rpx;
+
+ .icon {
+ font-size: 60rpx;
+ margin-bottom: 10rpx;
+ }
+ }
+ }
+
+ .image-hint {
+ font-size: 24rpx;
+ color: #999;
+ margin-top: 10rpx;
+ text-align: left;
+ }
+
+ .card-style-list {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+
+ .card-style-item {
+ position: relative;
+ width: 220rpx;
+ height: 320rpx;
+ border: 2rpx solid #e0e0e0;
+ border-radius: 10rpx;
+ overflow: hidden;
+
+ &.active {
+ border-color: #37bde6;
+ }
+
+ image {
+ width: 100%;
+ height: 100%;
+ }
+
+ .check-icon {
+ position: absolute;
+ top: 10rpx;
+ right: 10rpx;
+ width: 36rpx;
+ height: 36rpx;
+ background: #37bde6;
+ border-radius: 50%;
+ &::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) rotate(45deg);
+ width: 12rpx;
+ height: 20rpx;
+ border-right: 4rpx solid #fff;
+ border-bottom: 4rpx solid #fff;
+ }
+ }
+ }
+ }
+
+ .picker {
+ font-size: 28rpx;
+ color: #666;
+ padding: 10rpx 0;
+ }
+ }
+ }
+ }
+ }
+
+ .save-btn-container {
+ padding: 30rpx 0;
+
+ .save-btn {
+ width: 100%;
+ height: 90rpx;
+ background: #37bde6;
+ color: #fff;
+ font-size: 32rpx;
+ border-radius: 45rpx;
+ line-height: 90rpx;
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/mobile/pages/plus/business/visitors.vue b/mobile/pages/plus/business/visitors.vue
new file mode 100644
index 0000000..b2b1703
--- /dev/null
+++ b/mobile/pages/plus/business/visitors.vue
@@ -0,0 +1,288 @@
+<template>
+ <view>
+ <header-bar title="访客记录" :isBack="true" @click="back"></header-bar>
+ <view class="content">
+ <!-- 搜索栏 -->
+ <view class="search-bar">
+ <view class="search-input">
+ <text class="icon iconfont icon-sousuo"></text>
+ <input type="text" v-model="search" placeholder="搜索访客姓名" @confirm="searchVisitor" />
+ </view>
+ </view>
+
+ <!-- 访客列表 -->
+ <scroll-view scroll-y="true" class="visitor-list" @scrolltolower="loadMore">
+ <view v-if="visitors.length > 0">
+ <view class="visitor-item" v-for="(visitor, index) in visitors" :key="index">
+ <image class="visitor-avatar" :src="visitor.avatar || '/static/default.png'" mode="aspectFill"></image>
+ <view class="visitor-info">
+ <view class="visitor-header">
+ <view class="visitor-name">{{visitor.user_name || '未知访客'}}</view>
+ <view class="visitor-time">{{formatTime(visitor.visit_time)}}</view>
+ </view>
+ <view v-if="visitor.company_name" class="visitor-company">{{visitor.company_name}}</view>
+ <view v-if="visitor.position" class="visitor-position">{{visitor.position}}</view>
+ <view class="visitor-action">
+ <view class="action-btn" @click="viewCard(visitor.business_card_id)">
+ <text class="icon iconfont icon-card"></text>
+ <text>查看名片</text>
+ </view>
+ <view class="action-btn" @click="contactVisitor(visitor)">
+ <text class="icon iconfont icon-message"></text>
+ <text>联系访客</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 无数据提示 -->
+ <view v-else-if="!loading" class="no-data">
+ <text class="icon iconfont icon-wushuju"></text>
+ <text class="text">暂无访客记录</text>
+ </view>
+
+ <!-- 加载中 -->
+ <view v-if="loading && visitors.length > 0" class="loading-more">
+ <text>加载中...</text>
+ </view>
+
+ <!-- 无更多数据 -->
+ <view v-if="!hasMore && visitors.length > 0" class="no-more">
+ <text>没有更多了</text>
+ </view>
+ </scroll-view>
+ </view>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ visitors: [],
+ loading: false,
+ page: 1,
+ list_rows: 10,
+ hasMore: true,
+ search: ''
+ };
+ },
+ onLoad() {
+ this.getVisitors();
+ },
+ methods: {
+ back() {
+ uni.navigateBack();
+ },
+ // 获取访客列表
+ getVisitors() {
+ let _this = this;
+ _this.loading = true;
+ const params = {
+ page: _this.page,
+ list_rows: _this.list_rows
+ };
+ if (_this.search) {
+ params.search = _this.search;
+ }
+ _this._post('plus.business/business/getVisitors', params, function(res) {
+ _this.loading = false;
+ if (_this.page === 1) {
+ _this.visitors = res.data.list;
+ } else {
+ _this.visitors = _this.visitors.concat(res.data.list);
+ }
+ // 判断是否还有更多数据
+ _this.hasMore = _this.visitors.length < res.data.total;
+ });
+ },
+ // 搜索访客
+ searchVisitor() {
+ this.page = 1;
+ this.getVisitors();
+ },
+ // 加载更多
+ loadMore() {
+ if (!this.loading && this.hasMore) {
+ this.page++;
+ this.getVisitors();
+ }
+ },
+ // 查看访客名片
+ viewCard(business_card_id) {
+ this.gotoPage(`/pages/plus/business/detail?business_card_id=${business_card_id}`);
+ },
+ // 联系访客
+ contactVisitor(visitor) {
+ // 这里可以根据系统功能扩展,比如发送消息、拨打电话等
+ uni.showActionSheet({
+ itemList: ['发送消息', '拨打电话'],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ // 发送消息
+ this.showError('消息功能暂未开放');
+ } else if (res.tapIndex === 1) {
+ // 拨打电话
+ if (visitor.phone) {
+ uni.makePhoneCall({
+ phoneNumber: visitor.phone
+ });
+ } else {
+ this.showError('暂无联系电话');
+ }
+ }
+ }
+ });
+ },
+ // 格式化时间
+ formatTime(time) {
+ if (!time) return '';
+ const date = new Date(time);
+ const now = new Date();
+ const diff = now - date;
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+ if (days === 0) {
+ return '今天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
+ } else if (days === 1) {
+ return '昨天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
+ } else if (days < 7) {
+ return days + '天前';
+ } else {
+ return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+ .content {
+ padding: 20rpx;
+ }
+
+ .search-bar {
+ background: #fff;
+ padding: 20rpx;
+ border-radius: 10rpx;
+ margin-bottom: 20rpx;
+
+ .search-input {
+ display: flex;
+ align-items: center;
+ background: #f5f5f5;
+ border-radius: 60rpx;
+ padding: 0 30rpx;
+
+ .icon {
+ font-size: 32rpx;
+ color: #999;
+ margin-right: 20rpx;
+ }
+
+ input {
+ flex: 1;
+ height: 80rpx;
+ font-size: 28rpx;
+ color: #333;
+ }
+ }
+ }
+
+ .visitor-list {
+ height: calc(100vh - 200rpx);
+
+ .visitor-item {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ display: flex;
+
+ .visitor-avatar {
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 50%;
+ background: #f5f5f5;
+ }
+
+ .visitor-info {
+ flex: 1;
+ margin-left: 30rpx;
+
+ .visitor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10rpx;
+
+ .visitor-name {
+ font-size: 34rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .visitor-time {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+
+ .visitor-company,
+ .visitor-position {
+ font-size: 28rpx;
+ color: #666;
+ margin-bottom: 5rpx;
+ }
+
+ .visitor-action {
+ display: flex;
+ margin-top: 20rpx;
+
+ .action-btn {
+ display: flex;
+ align-items: center;
+ margin-right: 40rpx;
+
+ .icon {
+ font-size: 28rpx;
+ color: #37bde6;
+ margin-right: 8rpx;
+ }
+
+ text {
+ font-size: 26rpx;
+ color: #37bde6;
+ }
+ }
+ }
+ }
+ }
+
+ .no-data {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 400rpx;
+
+ .icon {
+ font-size: 120rpx;
+ color: #ccc;
+ margin-bottom: 30rpx;
+ }
+
+ .text {
+ font-size: 30rpx;
+ color: #999;
+ }
+ }
+
+ .loading-more,
+ .no-more {
+ text-align: center;
+ padding: 30rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/shop_vue/src/api/business.js b/shop_vue/src/api/business.js
new file mode 100644
index 0000000..2f12914
--- /dev/null
+++ b/shop_vue/src/api/business.js
@@ -0,0 +1,55 @@
+import request from '@/utils/request';
+
+let BusinessApi = {
+
+ /*模板列表*/
+ templateList(data, errorback) {
+ return request._post('/shop/plus.business.template/index', data, errorback);
+ },
+ /*模板详情*/
+ templateDetail(data, errorback) {
+ return request._get('/shop/plus.business.template/edit', data, errorback);
+ },
+ /*添加模板*/
+ templateAdd(data, errorback) {
+ return request._post('/shop/plus.business.template/add', data, errorback);
+ },
+ /*保存模板*/
+ templateSave(data, errorback) {
+ return request._post('/shop/plus.business.template/add', data, errorback);
+ },
+ /*修改模板*/
+ templateEdit(data, errorback) {
+ return request._post('/shop/plus.business.template/edit', data, errorback);
+ },
+ /*删除模板*/
+ templateDelete(data, errorback) {
+ return request._post('/shop/plus.business.template/delete', data, errorback);
+ },
+ /*设置默认模板*/
+ templateDefault(data, errorback) {
+ return request._get('/shop/plus.business.template/add', data, errorback);
+ },
+ /*名片管理列表*/
+ businessList(data, errorback) {
+ return request._post('/shop/plus.business.business/index', data, errorback);
+ },
+ /*名片详情*/
+ businessDetail(data, errorback) {
+ return request._get('/shop/plus.business.business/edit', data, errorback);
+ },
+ /*添加名片*/
+ businessAdd(data, errorback) {
+ return request._post('/shop/plus.business.business/add', data, errorback);
+ },
+ /*修改名片*/
+ businessEdit(data, errorback) {
+ return request._post('/shop/plus.business.business/edit', data, errorback);
+ },
+ /*删除名片*/
+ businessDelete(data, errorback) {
+ return request._post('/shop/plus.business.business/delete', data, errorback);
+ }
+};
+
+export default BusinessApi;
\ No newline at end of file
diff --git a/shop_vue/src/views/plus/business/business/index.vue b/shop_vue/src/views/plus/business/business/index.vue
new file mode 100644
index 0000000..cee12a5
--- /dev/null
+++ b/shop_vue/src/views/plus/business/business/index.vue
@@ -0,0 +1,7 @@
+<template>
+
+</template>
+
+<style scoped>
+
+</style>
diff --git a/shop_vue/src/views/plus/business/grade/index.vue b/shop_vue/src/views/plus/business/grade/index.vue
new file mode 100644
index 0000000..87df71a
--- /dev/null
+++ b/shop_vue/src/views/plus/business/grade/index.vue
@@ -0,0 +1,278 @@
+<template>
+ <!--
+ 作者:系统
+ 时间:2025-10-28
+ 描述:名片等级管理
+ -->
+ <div class="common-seach-wrap">
+ <div class="search-left">
+ <el-button type="primary" @click="addGrade" v-auth="'/plus/business/grade/add'">添加等级</el-button>
+ </div>
+
+ <div class="table-container">
+ <el-table v-loading="loading" :data="listData" border>
+ <el-table-column prop="grade_id" label="等级ID" width="80" align="center"></el-table-column>
+ <el-table-column prop="name" label="等级名称" align="center"></el-table-column>
+ <el-table-column prop="price" label="查看联系方式价格" align="center">
+ <template slot-scope="scope">
+ {{ scope.row.price>0 ? '¥' + scope.row.price : '免费' }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="weight" label="权重" align="center">
+ <!--<template slot-scope="scope">
+ <el-input-number
+ v-model="scope.row.weight"
+ :min="0"
+ size="mini"
+ @change="handleWeightChange(scope.row)"
+ v-auth="'/plus/business/grade/edit'"
+ ></el-input-number>
+ </template>-->
+ </el-table-column>
+ <el-table-column prop="create_time" label="创建时间" width="180" align="center">
+ </el-table-column>
+ <el-table-column prop="update_time" label="更新时间" width="180" align="center">
+ </el-table-column>
+ <el-table-column label="操作" width="180" align="center">
+ <template slot-scope="scope">
+ <el-button
+ type="text"
+ @click="editGrade(scope.row)"
+ v-auth="'/plus/business/grade/edit'"
+ >编辑</el-button>
+ <el-button
+ type="text"
+ @click="deleteGrade(scope.row)"
+ v-auth="'/plus/business/grade/delete'"
+ >删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 添加/编辑等级对话框 -->
+ <el-dialog
+ :title="dialogTitle"
+ :visible.sync="dialogVisible"
+ width="500px"
+ @close="handleClose"
+ >
+ <el-form :model="formData" :rules="rules" ref="formData" label-width="150px">
+ <el-form-item label="等级名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入等级名称"></el-input>
+ </el-form-item>
+ <el-form-item label="查看联系方式价格" prop="price">
+ <el-input
+ v-model.number="formData.price"
+ placeholder="请输入价格,0表示免费"
+ type="number"
+ :min="0"
+ :step="0.01"
+ ></el-input>
+ </el-form-item>
+ <el-form-item label="权重" prop="weight">
+ <el-input-number
+ v-model="formData.weight"
+ :min="0"
+ placeholder="权重越小,排序越靠前"
+ ></el-input-number>
+ </el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="dialogVisible = false">取消</el-button>
+ <el-button type="primary" @click="submitForm">确定</el-button>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import PlusApi from '@/api/plus.js';
+
+export default {
+ data() {
+ return {
+ loading: false,
+ listData: [],
+ dialogVisible: false,
+ dialogTitle: '',
+ formData: {
+ grade_id: '',
+ name: '',
+ price: 0.00,
+ weight: 100
+ },
+ rules: {
+ name: [
+ { required: true, message: '请输入等级名称', trigger: 'blur' },
+ { min: 1, max: 20, message: '等级名称长度在 1 到 20 个字符', trigger: 'blur' }
+ ],
+ price: [
+ { type: 'number', min: 0, message: '价格不能小于0', trigger: 'blur' }
+ ],
+ weight: [
+ { type: 'number', required: true, message: '请输入权重', trigger: 'blur' },
+ { type: 'number', min: 0, message: '权重不能小于0', trigger: 'blur' }
+ ]
+ }
+ };
+ },
+ mounted() {
+ this.loadData();
+ },
+ methods: {
+ // 加载数据
+ loadData() {
+ this.loading = true;
+ PlusApi.gradeIndex().then(res => {
+ this.loading = false;
+ this.listData = res.data.list || [];
+ console.log(this.listData);
+
+ }).catch(() => {
+ this.loading = false;
+ this.$message.error('获取数据失败');
+ });
+ },
+
+ // 添加等级
+ addGrade() {
+ this.dialogTitle = '添加等级';
+ this.formData = {
+ grade_id: '',
+ name: '',
+ price: 0.00,
+ weight: 100
+ };
+ this.dialogVisible = true;
+ },
+
+ // 编辑等级
+ editGrade(row) {
+ this.dialogTitle = '编辑等级';
+ this.formData = {
+ grade_id: row.grade_id,
+ name: row.name,
+ price: parseFloat(row.price) || 0.00,
+ weight: parseInt(row.weight) || 100
+ };
+ this.dialogVisible = true;
+ },
+
+ // 关闭对话框
+ handleClose() {
+ this.$refs.formData.resetFields();
+ },
+
+ // 提交表单
+ submitForm() {
+ this.$refs.formData.validate((valid) => {
+ if (valid) {
+ const params = { ...this.formData };
+
+ if (params.grade_id) {
+ // 编辑
+ PlusApi.gradeEdit(params).then(res => {
+ if (res.code === 1) {
+ this.$message.success('编辑成功');
+ this.dialogVisible = false;
+ this.loadData();
+ } else {
+ this.$message.error(res.msg || '编辑失败');
+ }
+ }).catch(() => {
+ this.$message.error('编辑失败');
+ });
+ } else {
+ // 添加
+ PlusApi.gradeAdd(params).then(res => {
+ if (res.code === 1) {
+ this.$message.success('添加成功');
+ this.dialogVisible = false;
+ this.loadData();
+ } else {
+ this.$message.error(res.msg || '添加失败');
+ }
+ }).catch(() => {
+ this.$message.error('添加失败');
+ });
+ }
+ }
+ });
+ },
+
+ // 删除等级
+ deleteGrade(row) {
+ this.$confirm('确定要删除这个等级吗?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ }).then(() => {
+ PlusApi.gradeDelete({ grade_id: row.grade_id }).then(res => {
+ if (res.code === 1) {
+ this.$message.success('删除成功');
+ this.loadData();
+ } else {
+ this.$message.error(res.msg || '删除失败');
+ }
+ }).catch(() => {
+ this.$message.error('删除失败');
+ });
+ }).catch(() => {});
+ },
+
+ // 处理权重变化
+ handleWeightChange(row) {
+ const params = {
+ grade_id: row.grade_id,
+ weight: row.weight,
+ name: row.name,
+ price: row.price
+ };
+
+ PlusApi.gradeEdit(params).then(res => {
+ if (res.code !== 1) {
+ this.$message.error(res.msg || '更新权重失败');
+ this.loadData(); // 重新加载数据
+ }
+ }).catch(() => {
+ this.$message.error('更新权重失败');
+ this.loadData();
+ });
+ },
+
+ // 格式化日期
+ formatDate(timestamp) {
+ if (!timestamp) return '';
+ const date = new Date(timestamp * 1000);
+ const year = date.getFullYear();
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const day = date.getDate().toString().padStart(2, '0');
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ const seconds = date.getSeconds().toString().padStart(2, '0');
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+ },
+ }
+};
+</script>
+
+<style scoped>
+.common-seach-wrap {
+ padding: 20px;
+ background-color: #fff;
+ min-height: calc(100vh - 60px);
+}
+
+.search-left {
+ margin-bottom: 20px;
+}
+
+.table-container {
+ margin-top: 10px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+</style>
\ No newline at end of file
diff --git a/shop_vue/src/views/plus/business/index.vue b/shop_vue/src/views/plus/business/index.vue
new file mode 100644
index 0000000..8462a52
--- /dev/null
+++ b/shop_vue/src/views/plus/business/index.vue
@@ -0,0 +1,135 @@
+<template>
+ <!--
+ 作者:luoyiming
+ 时间:2019-06-04
+ 描述:插件中心-分销
+ -->
+ <div class="common-seach-wrap">
+ <!--名片列表-->
+ <Business v-if="activeName == 'business'"></Business>
+ <!--名片模板记录-->
+ <Template v-if="activeName == 'template'"></Template>
+ <!--名片等级管理-->
+ <Grade v-if="activeName == 'grade'"></Grade>
+ <!--行业管理-->
+ <Industry v-if="activeName == 'industry'"></Industry>
+ </div>
+</template>
+<script>
+import bus from '@/utils/eventBus.js';
+import PlusApi from '@/api/plus.js';
+import Business from './business/index.vue';
+import Template from './template/index.vue';
+import Grade from './grade/index.vue';
+import Industry from './industry/index.vue';
+
+export default {
+ components: {
+ Business,
+ Template,
+ Grade,
+ Industry
+ },
+ data() {
+ return {
+ formInline: {
+ nick_name: ''
+ },
+ /*参数*/
+ param: {},
+ /*当前选中*/
+ activeName: 'business',
+ /*切换数组*/
+ sourceList: [
+ {
+ key: 'industry',
+ value: '行业管理',
+ path: '/plus/business/industry/index',
+
+ },
+ {
+ key: 'business',
+ value: '名片列表',
+ path:'/plus/business/business/index'
+ },
+ {
+ key: 'template',
+ value: '名片模板记录',
+ path:'/plus/business/template/index'
+ },
+ {
+ key: 'grade',
+ value: '名片等级管理',
+ path:'/plus/business/grade/index'
+ }
+ ],
+ /*权限筛选后的数据*/
+ tabList:[],
+ /*判断third是否有参数*/
+ is_third_param: false
+ };
+ },
+ watch:{
+
+ //监听路由
+ $route(to, from) {
+ this.init();
+ }
+ },
+ created() {
+
+ this.init();
+
+ },
+ beforeDestroy() {
+ //发送类别切换
+ bus.$emit('tabData', { active: null, tab_type:'business',list: [] });
+ bus.$off('activeValue');
+ },
+ methods: {
+
+ /*初始化方法*/
+ init(){
+ this.tabList=this.authFilter();
+
+ if(this.tabList.length>0){
+ this.activeName=this.tabList[0].key;
+ }
+
+ if (this.$route.query.type != null) {
+ this.activeName = this.$route.query.type;
+ }
+
+ /*监听传插件的值*/
+ bus.$on('activeValue', res => {
+ if (this.is_third_param) {
+ this.param.user_id = '';
+ this.is_third_param = false;
+ }
+ this.activeName = res;
+ });
+
+ //发送类别切换
+ let params = {
+ active: this.activeName,
+ list: this.tabList,
+ tab_type:'business'
+ };
+ bus.$emit('tabData', params);
+ },
+
+ /*权限过滤*/
+ authFilter(){
+ let list=[];
+ for(let i=0;i<this.sourceList.length;i++){
+ let item=this.sourceList[i];
+ if(this.$filter.isAuth(item.path)){
+ list.push(item);
+ }
+ }
+ return list;
+ }
+
+ }
+};
+</script>
diff --git a/shop_vue/src/views/plus/business/industry/Add.vue b/shop_vue/src/views/plus/business/industry/Add.vue
new file mode 100644
index 0000000..4fc8bb1
--- /dev/null
+++ b/shop_vue/src/views/plus/business/industry/Add.vue
@@ -0,0 +1,104 @@
+<template>
+ <!--
+ 描述:行业管理-添加
+ -->
+ <el-dialog title="添加行业" :visible.sync="dialogVisible" @close="dialogFormVisible" :close-on-click-modal="false"
+ :close-on-press-escape="false">
+ <el-form size="small" :model="form" :rules="formRules" ref="form">
+ <el-form-item label="所属行业" :label-width="formLabelWidth">
+ <el-select v-model="form.parent_id">
+ <el-option label="顶级行业" value="0"></el-option>
+ <el-option :value="cat.industry_id" :label="cat.name" :key="cat.industry_id" v-for="cat in addform.industryOptions"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="行业名称" prop="name" :label-width="formLabelWidth">
+ <el-input v-model="form.name" autocomplete="off"></el-input>
+ </el-form-item>
+ <el-form-item label="排序" prop="sort" :label-width="formLabelWidth">
+ <el-input v-model.number="form.sort" autocomplete="off"></el-input>
+ </el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="dialogFormVisible">取 消</el-button>
+ <el-button type="primary" @click="addIndustry" :loading="loading">确 定</el-button>
+ </div>
+ </el-dialog>
+</template>
+
+<script>
+ import PlusApi from '@/api/plus';
+ export default {
+ data() {
+ return {
+ form: {
+ parent_id: '0',
+ name: '',
+ sort: 100
+ },
+ formRules: {
+ name: [{
+ required: true,
+ message: '请输入行业名称',
+ trigger: 'blur'
+ }],
+ sort: [{
+ required: true,
+ message: '排序不能为空'
+ }, {
+ type: 'number',
+ message: '排序必须为数字'
+ }]
+ },
+ /*左边长度*/
+ formLabelWidth: '120px',
+ /*是否显示*/
+ dialogVisible: false,
+ loading: false
+ };
+ },
+ props: ['open_add', 'addform'],
+ created() {
+ this.dialogVisible = this.open_add;
+ },
+ methods: {
+ /*添加行业*/
+ addIndustry() {
+ let self = this;
+ let params = self.form;
+ self.$refs.form.validate((valid) => {
+ if (valid) {
+ self.loading = true;
+ PlusApi.addIndustry(params).then(data => {
+ self.loading = false;
+ self.$message({
+ message: '添加成功',
+ type: 'success'
+ });
+ self.dialogFormVisible(true);
+ }).catch(error => {
+ self.loading = false;
+ });
+ }
+ });
+ },
+
+ /*关闭弹窗*/
+ dialogFormVisible(e) {
+ if (e) {
+ this.$emit('closeDialog', {
+ type: 'success',
+ openDialog: false
+ })
+ } else {
+ this.$emit('closeDialog', {
+ type: 'error',
+ openDialog: false
+ })
+ }
+ }
+ }
+ };
+</script>
+
+<style>
+</style>
\ No newline at end of file
diff --git a/shop_vue/src/views/plus/business/industry/Edit.vue b/shop_vue/src/views/plus/business/industry/Edit.vue
new file mode 100644
index 0000000..b56d7a0
--- /dev/null
+++ b/shop_vue/src/views/plus/business/industry/Edit.vue
@@ -0,0 +1,108 @@
+<template>
+ <!--
+ 描述:行业管理-修改
+ -->
+ <el-dialog title="修改行业" :visible.sync="dialogVisible" @close="dialogFormVisible" :close-on-click-modal="false"
+ :close-on-press-escape="false">
+ <el-form size="small" :model="form" :rules="formRules" ref="form">
+ <el-form-item label="所属行业" :label-width="formLabelWidth">
+ <el-select v-model="form.parent_id">
+ <el-option label="顶级行业" :value="0"></el-option>
+ <el-option :value="cat.industry_id" :label="cat.name" :key="cat.industry_id" v-for="cat in editform.industryOptions"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="行业名称" prop="name" :label-width="formLabelWidth">
+ <el-input v-model="form.name" autocomplete="off"></el-input>
+ </el-form-item>
+ <el-form-item label="排序" prop="sort" :label-width="formLabelWidth">
+ <el-input v-model.number="form.sort" autocomplete="off"></el-input>
+ </el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="dialogFormVisible">取 消</el-button>
+ <el-button type="primary" @click="editIndustry" :loading="loading">确 定</el-button>
+ </div>
+ </el-dialog>
+</template>
+
+<script>
+ import PlusApi from '@/api/plus';
+ export default {
+ data() {
+ return {
+ form: {
+ industry_id: 0,
+ parent_id: 0,
+ name: '',
+ sort: ''
+ },
+ formRules: {
+ name: [{
+ required: true,
+ message: '请输入行业名称',
+ trigger: 'blur'
+ }],
+ sort: [{
+ required: true,
+ message: '排序不能为空'
+ }, {
+ type: 'number',
+ message: '排序必须为数字'
+ }]
+ },
+ /*左边长度*/
+ formLabelWidth: '120px',
+ /*是否显示*/
+ dialogVisible: false,
+ loading: false
+ };
+ },
+ props: ['open_edit', 'editform'],
+ created() {
+ this.dialogVisible = this.open_edit;
+ this.form.industry_id = this.editform.model.industry_id;
+ this.form.parent_id = this.editform.model.parent_id;
+ this.form.name = this.editform.model.name;
+ this.form.sort = this.editform.model.sort;
+ },
+ methods: {
+ /*修改行业*/
+ editIndustry() {
+ let self = this;
+ let params = self.form;
+ self.$refs.form.validate((valid) => {
+ if (valid) {
+ self.loading = true;
+ PlusApi.editIndustry(params, true).then(data => {
+ self.loading = false;
+ self.$message({
+ message: '修改成功',
+ type: 'success'
+ });
+ self.dialogFormVisible(true);
+ }).catch(error => {
+ self.loading = false;
+ });
+ }
+ });
+ },
+ /*关闭弹窗*/
+ dialogFormVisible(e) {
+ if (e) {
+ this.$emit('closeDialog', {
+ type: 'success',
+ openDialog: false
+ })
+ } else {
+ this.$emit('closeDialog', {
+ type: 'error',
+ openDialog: false
+ })
+ }
+ }
+ }
+ };
+</script>
+
+<style>
+</style>
\ No newline at end of file
diff --git a/shop_vue/src/views/plus/business/industry/index.vue b/shop_vue/src/views/plus/business/industry/index.vue
new file mode 100644
index 0000000..f1212c8
--- /dev/null
+++ b/shop_vue/src/views/plus/business/industry/index.vue
@@ -0,0 +1,179 @@
+<template>
+ <!--
+ 作者:wangxw
+ 时间:2023-10-26
+ 描述:行业管理
+ -->
+ <div class="product">
+
+ <!--添加行业-->
+ <div class="common-level-rail">
+ <el-button size="small" type="primary" @click="addClick" icon="el-icon-plus" v-auth="'/plus/business/industry/add'">添加行业</el-button>
+ </div>
+
+ <!--内容-->
+ <div class="product-content">
+ <div class="table-wrap">
+ <el-table size="small" :data="tableData" row-key="industry_id" default-expand-all :tree-props="{children: 'child'}"
+ style="width: 100%" v-loading="loading">
+ <el-table-column prop="industry_id" label="行业ID" width="80" align="center"></el-table-column>
+ <el-table-column prop="name" label="行业名称" width="180"></el-table-column>
+ <el-table-column prop="sort" label="排序"></el-table-column>
+ <el-table-column prop="create_time" label="创建时间"></el-table-column>
+ <el-table-column prop="status" label="状态">
+ <template slot-scope="scope">
+ <el-checkbox v-model="scope.row.status" :checked="scope.row.status" @change="checked => statusChange(checked, scope.row)">启用</el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="操作" width="100">
+ <template slot-scope="scope">
+ <el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/plus/business/industry/edit'">编辑</el-button>
+ <el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/plus/business/industry/delete'">删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <!--添加-->
+ <Add v-if="open_add" :open_add="open_add" :addform="industryModel" @closeDialog="closeDialogFunc($event, 'add')"></Add>
+ <!--修改-->
+ <Edit v-if="open_edit" :open_edit="open_edit" :editform="industryModel" @closeDialog="closeDialogFunc($event, 'edit')"></Edit>
+ </div>
+</template>
+
+<script>
+import PlusApi from '@/api/plus';
+import Add from './Add.vue';
+import Edit from './Edit.vue';
+export default {
+ components: {
+ Add,
+ Edit
+ },
+ data() {
+ return {
+ /*是否加载完成*/
+ loading: true,
+ /*列表数据*/
+ tableData: [],
+ /*是否打开添加弹窗*/
+ open_add: false,
+ /*是否打开编辑弹窗*/
+ open_edit: false,
+ /*当前编辑的对象*/
+ industryModel: {
+ industryOptions: [],
+ model: {}
+ }
+ };
+ },
+ created() {
+ /*获取列表*/
+ this.getData();
+ },
+ methods: {
+ /*获取列表*/
+ getData() {
+ let self = this;
+ PlusApi.getIndustryList({}, true)
+ .then(data => {
+ self.loading = false;
+ self.tableData = data.data.list.tree;
+ self.industryModel.industryOptions = data.data.list.tree;
+ })
+ .catch(error => {
+ self.loading = false;
+ });
+ },
+ /*打开添加*/
+ addClick() {
+ this.open_add = true;
+ },
+
+ /*打开编辑*/
+ editClick(item) {
+ this.industryModel.model = item;
+ this.open_edit = true;
+ },
+
+ /*关闭弹窗*/
+ closeDialogFunc(e, f) {
+ if (f == 'add') {
+ this.open_add = e.openDialog;
+ if (e.type == 'success') {
+ this.getData();
+ }
+ }
+ if (f == 'edit') {
+ this.open_edit = e.openDialog;
+ if (e.type == 'success') {
+ this.getData();
+ }
+ }
+ },
+ /*删除行业*/
+ deleteClick(row) {
+ let self = this;
+ self.$confirm('删除后不可恢复,确认删除该记录吗?', '提示', {
+ type: 'warning'
+ }).then(() => {
+ PlusApi.deleteIndustry({
+ industry_id: row.industry_id
+ }).then(data => {
+ self.$message({
+ message: '删除成功',
+ type: 'success'
+ });
+ self.getData();
+ });
+ });
+ },
+ /*启用/禁用*/
+ statusChange: function(checked, row) {
+ let self = this;
+ let status = checked ? 1 : 0;
+ self.loading = true;
+ let params = {
+ industry_id: row.industry_id,
+ parent_id: row.parent_id,
+ name: row.name,
+ sort: row.sort,
+ status: status
+ };
+ PlusApi.editIndustry(
+ params,
+ true
+ )
+ .then(data => {
+ self.loading = false;
+ row.status = checked;
+ })
+ .catch(error => {
+ self.loading = false;
+ row.status = checked ? 0 : 1;
+ });
+ },
+ }
+};
+</script>
+
+<style scoped>
+.product {
+ padding: 20px;
+}
+
+.common-level-rail {
+ margin-bottom: 20px;
+}
+
+.product-content {
+ clear: both;
+}
+
+.table-wrap {
+ background: #fff;
+ padding: 20px;
+ border-radius: 4px;
+}
+</style>
\ No newline at end of file
diff --git a/shop_vue/src/views/plus/business/template/add.vue b/shop_vue/src/views/plus/business/template/add.vue
new file mode 100644
index 0000000..29d388e
--- /dev/null
+++ b/shop_vue/src/views/plus/business/template/add.vue
@@ -0,0 +1,993 @@
+<template>
+ <!--
+ 作者:系统自动生成
+ 时间:当前日期
+ 描述:插件中心-名片模板-添加模板
+ -->
+ <div class="user" v-loading="loading">
+ <div class="common-form">名片模板添加</div>
+ <div class="poster-box d-s-s">
+ <div class="left-box">
+ <div v-if="form.backdrop" class="img"><img v-img-url="form.backdrop.src" /></div>
+ <div class="userinfo">
+ <!-- 头像 -->
+ <div v-if="form.avatar.display == 1"
+ class="photo pa"
+ v-drag="{type:'avatar',obj:this}"
+ :class="{ radius: form.avatar.style == 'circle' }"
+ :style="'width:' + form.avatar.width + 'px;height:' + form.avatar.width + 'px;top:' + form.avatar.top + 'px;left:' + form.avatar.left + 'px;background-color:#f0f0f0;display:flex;align-items:center;justify-content:center;color:#999'">
+ <span>头像</span>
+ </div>
+
+ <!-- Logo -->
+ <div v-if="form.logo.display == 1"
+ class="logo pa"
+ v-drag="{type:'logo',obj:this}"
+ :class="{ radius: form.logo.style == 'circle' }"
+ :style="'width:' + form.logo.width + 'px;height:' + form.logo.height + 'px;top:' + form.logo.top + 'px;left:' + form.logo.left + 'px;'">
+ <img v-img-url="form.logo.src" alt="" />
+ </div>
+
+ <!-- 姓名 -->
+ <div class="name pa"
+ v-drag="{type:'name',obj:this}"
+ :style="'font-size:' + form.name.fontSize + 'px;color:' + form.name.color + ';top:' + form.name.top + 'px;left:' + form.name.left + 'px;'">
+ 这里是姓名
+ </div>
+
+ <!-- 手机 -->
+ <div class="mobile pa"
+ v-drag="{type:'mobile',obj:this}"
+ :style="'font-size:' + form.mobile.fontSize + 'px;color:' + form.mobile.color + ';top:' + form.mobile.top + 'px;left:' + form.mobile.left + 'px;'">
+ 手机:134xxxxxxxx
+ </div>
+
+ <!-- 公司 -->
+ <div v-for="(item, index) in form.unit" :key="'unit' + index"
+ :class="'unit' + index + ' pa'"
+ v-drag="{type:'unit', index:index, obj:this}"
+ :style="'font-size:' + form.unit[index].fontSize + 'px;color:' + form.unit[index].color + ';top:' + form.unit[index].top + 'px;left:' + form.unit[index].left + 'px;'">
+ 这是公司{{index+1}}
+ </div>
+
+ <!-- 职位 -->
+ <div v-for="(item, index) in form.duties" :key="'duties' + index"
+ :class="'duties' + index + ' pa'"
+ v-drag="{type:'duties', index:index, obj:this}"
+ :style="'font-size:' + form.duties[index].fontSize + 'px;color:' + form.duties[index].color + ';top:' + form.duties[index].top + 'px;left:' + form.duties[index].left + 'px;'">
+ 这是职位{{index+1}}
+ </div>
+
+ <!-- 地址 -->
+ <div v-for="(item, index) in form.address" :key="'address' + index"
+ :class="'address' + index + ' pa'"
+ v-drag="{type:'address', index:index, obj:this}"
+ :style="'font-size:' + form.address[index].fontSize + 'px;color:' + form.address[index].color + ';top:' + form.address[index].top + 'px;left:' + form.address[index].left + 'px;'">
+ 地址:广西壮族自治区南宁市江南区壮锦大道八桂绿城·龙湖御景-A栋-2单元{{index+1}}号
+ </div>
+
+ <!-- 微信 -->
+ <div class="wechat pa"
+ v-drag="{type:'wechat',obj:this}"
+ :style="'font-size:' + form.wechat.fontSize + 'px;color:' + form.wechat.color + ';top:' + form.wechat.top + 'px;left:' + form.wechat.left + 'px;'">
+ 微信:134xxxxxxxx
+ </div>
+
+ <!-- 邮箱 -->
+ <div class="mailbox pa"
+ v-drag="{type:'mailbox',obj:this}"
+ :style="'font-size:' + form.mailbox.fontSize + 'px;color:' + form.mailbox.color + ';top:' + form.mailbox.top + 'px;left:' + form.mailbox.left + 'px;'">
+ 邮箱:134xxxxxxxx@.xxx.com
+ </div>
+
+ <!-- 电话 -->
+ <div class="phone pa"
+ v-drag="{type:'phone',obj:this}"
+ :style="'font-size:' + form.phone.fontSize + 'px;color:' + form.phone.color + ';top:' + form.phone.top + 'px;left:' + form.phone.left + 'px;'">
+ 电话:xxx-xxx-xxx
+ </div>
+
+ <!-- 自定义图标 -->
+ <div v-for="(item, index) in form.iconL" :key="'iconL' + index"
+ :class="'iconL' + index + ' pa icon'"
+ v-drag="{type:'iconL', index:index, obj:this}"
+ :style="'width:' + form.iconL[index].width + 'px;height:' + form.iconL[index].height + 'px;top:' + form.iconL[index].top + 'px;left:' + form.iconL[index].left + 'px;'">
+ <img v-img-url="form.iconL[index].src" alt="" />
+ </div>
+ </div>
+ </div>
+
+ <div class="right-box flex-1">
+ <el-form size="small" ref="form" :model="form" label-width="150px">
+ <!-- 背景图 -->
+ <el-form-item label="海报背景图">
+ <el-button type="primary" @click="openUpload(1)">上传图片</el-button>
+ <div class="tips">尺寸建议:宽750像素 高大于(等于)1200像素</div>
+ </el-form-item>
+
+ <!-- 头像设置 -->
+ <el-form-item label="是否显示头像">
+ <el-radio v-model="form.avatar.display" label="1">显示</el-radio>
+ <el-radio v-model="form.avatar.display" label="0">隐藏</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.avatar.display == 1" label="头像宽度" prop="avatar.width" :rules="[{ required: true, message: '请输入头像宽度' }]">
+ <el-input v-model.number="form.avatar.width" min="30" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item v-if="form.avatar.display == 1" label="头像样式">
+ <el-radio v-model="form.avatar.style" label="square">正方形</el-radio>
+ <el-radio v-model="form.avatar.style" label="circle">圆形</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.avatar.display == 1" label="头像位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.avatar.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.avatar.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- Logo设置 -->
+ <el-form-item label="是否显示Logo">
+ <el-radio v-model="form.logo.display" label="1">显示</el-radio>
+ <el-radio v-model="form.logo.display" label="0">隐藏</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo图片">
+ <el-button type="primary" @click="openUpload(2)">上传图片</el-button>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo尺寸" prop="logo.width" :rules="[{ required: true, message: '请输入Logo宽度' }]">
+ <div class="d-s-r">
+ <el-input v-model.number="form.logo.width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.logo.height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
+ </div>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo样式">
+ <el-radio v-model="form.logo.style" label="square">正方形</el-radio>
+ <el-radio v-model="form.logo.style" label="circle">圆形</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.logo.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.logo.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 姓名设置 -->
+ <el-form-item label="姓名字体大小" prop="name.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
+ <el-input v-model.number="form.name.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="姓名字体颜色">
+ <el-color-picker v-model="form.name.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="姓名位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.name.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.name.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 手机设置 -->
+ <el-form-item label="手机字体大小" prop="mobile.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
+ <el-input v-model.number="form.mobile.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="手机字体颜色">
+ <el-color-picker v-model="form.mobile.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="手机位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.mobile.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.mobile.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 图标数量设置 -->
+ <el-form-item label="图标数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editIcon" :disabled="form.iconL.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.iconL.length }}</span>
+ <el-button type="text" @click="addIcon">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 图标设置 -->
+ <div v-for="(item, index) in form.iconL" :key="'icon_setting' + index" class="icon-setting">
+ <el-form-item :label="'图标' + (index + 1)">
+ <el-button type="primary" @click="openUpload(3, index)">上传图片</el-button>
+ </el-form-item>
+ <el-form-item :label="'图标' + (index + 1) + '尺寸'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.iconL[index].width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.iconL[index].height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
+ </div>
+ </el-form-item>
+ <el-form-item :label="'图标' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.iconL[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.iconL[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 公司数量设置 -->
+ <el-form-item label="公司数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editUnit" :disabled="form.unit.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.unit.length }}</span>
+ <el-button type="text" @click="addUnit">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 公司设置 -->
+ <div v-for="(item, index) in form.unit" :key="'unit_setting' + index" class="unit-setting">
+ <el-form-item :label="'公司' + (index + 1) + '字体大小'">
+ <el-input v-model.number="form.unit[index].fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item :label="'公司' + (index + 1) + '字体颜色'">
+ <el-color-picker v-model="form.unit[index].color"></el-color-picker>
+ </el-form-item>
+ <el-form-item :label="'公司' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.unit[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.unit[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 职位数量设置 -->
+ <el-form-item label="职位数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editDuties" :disabled="form.duties.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.duties.length }}</span>
+ <el-button type="text" @click="addDuties">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 职位设置 -->
+ <div v-for="(item, index) in form.duties" :key="'duties_setting' + index" class="duties-setting">
+ <el-form-item :label="'职位' + (index + 1) + '字体大小'">
+ <el-input v-model.number="form.duties[index].fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item :label="'职位' + (index + 1) + '字体颜色'">
+ <el-color-picker v-model="form.duties[index].color"></el-color-picker>
+ </el-form-item>
+ <el-form-item :label="'职位' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.duties[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.duties[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 地址数量设置 -->
+ <el-form-item label="地址数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editAddress" :disabled="form.address.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.address.length }}</span>
+ <el-button type="text" @click="addAddress">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 地址设置 -->
+ <div v-for="(item, index) in form.address" :key="'address_setting' + index" class="address-setting">
+ <el-form-item :label="'地址' + (index + 1) + '字体大小'">
+ <el-input v-model.number="form.address[index].fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item :label="'地址' + (index + 1) + '字体颜色'">
+ <el-color-picker v-model="form.address[index].color"></el-color-picker>
+ </el-form-item>
+ <el-form-item :label="'地址' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.address[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.address[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 微信设置 -->
+ <el-form-item label="微信字体大小">
+ <el-input v-model.number="form.wechat.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="微信字体颜色">
+ <el-color-picker v-model="form.wechat.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="微信位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.wechat.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.wechat.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 邮箱设置 -->
+ <el-form-item label="邮箱字体大小">
+ <el-input v-model.number="form.mailbox.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="邮箱字体颜色">
+ <el-color-picker v-model="form.mailbox.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="邮箱位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.mailbox.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.mailbox.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 电话设置 -->
+ <el-form-item label="电话字体大小">
+ <el-input v-model.number="form.phone.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="电话字体颜色">
+ <el-color-picker v-model="form.phone.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="电话位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.phone.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.phone.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 提交按钮 -->
+ <div class="common-button-wrapper">
+ <el-button @click="back">返回</el-button>
+ <el-button type="primary" @click="onSubmit" :loading="loading">提交</el-button>
+ </div>
+ </el-form>
+ </div>
+ </div>
+
+ <!-- 上传图片组件 -->
+ <Upload v-if="isupload" :isupload="isupload" :type="uploadType" :index="uploadIndex" @returnImgs="returnImgsFunc">上传图片</Upload>
+ </div>
+</template>
+
+<script>
+import Upload from '@/components/file/Upload';
+import BusinessApi from '@/api/business';
+
+export default {
+ components: {
+ Upload
+ },
+ data() {
+ return {
+ loading: false,
+ isupload: false,
+ uploadType: 1,
+ uploadIndex: 0,
+ form: {
+ backdrop: {
+ src: '',
+ height: 0,
+ width: 0,
+ type: 'backdrop'
+ },
+ is_business: 0,
+ name: {
+ fontSize: 14,
+ color: '#000000',
+ left: 232,
+ top: 13,
+ fontWeight: 400,
+ type: 'text'
+ },
+ avatar: {
+ width: 70,
+ style: 'circle',
+ left: 380,
+ top: 37,
+ display: '1',
+ src: '',
+ type: 'avatar'
+ },
+ logo: {
+ width: 70,
+ height: 27,
+ style: 'square',
+ left: 22,
+ top: 24,
+ display: '1',
+ src: '',
+ type: 'image'
+ },
+ mobile: {
+ fontSize: 14,
+ color: '#000000',
+ left: 192,
+ top: 43,
+ fontWeight: 400
+ },
+ address: [
+ {
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 206,
+ fontWeight: 400,
+ type: 'text'
+ }
+ ],
+ unit: [
+ {
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 167,
+ fontWeight: 100,
+ type: 'text'
+ }
+ ],
+ duties: [
+ {
+ fontSize: 14,
+ color: '#000000',
+ left: 260,
+ top: 167,
+ fontWeight: 400,
+ type: 'text'
+ }
+ ],
+ position: [],
+ wechat: {
+ fontSize: 14,
+ color: '#000000',
+ left: 205,
+ top: 65,
+ fontWeight: 400,
+ type: 'text'
+ },
+ mailbox: {
+ fontSize: 14,
+ color: '#000000',
+ left: 205,
+ top: 104,
+ fontWeight: 400,
+ type: 'text'
+ },
+ phone: {
+ fontSize: 14,
+ color: '#000000',
+ left: 205,
+ top: 84,
+ fontWeight: 400,
+ type: 'text'
+ },
+ positionNum: 0,
+ iconL: []
+ }
+ };
+ },
+ created() {
+ this.loadData();
+
+ // 在组件创建后初始化拖拽事件
+ this.$nextTick(() => {
+ // 通过遍历this.form对象动态调用dragEventArray方法
+ Object.keys(this.form).forEach(key => {
+ this.dragEventArray(key);
+ });
+ });
+ },
+ directives: {
+ drag: {
+ inserted: function(el, binding) {
+ // 为元素添加drag和对应的type类名,方便dragEventArray方法选择
+ const type = binding.value.type;
+ el.classList.add('drag');
+ el.classList.add(type);
+
+ // 避免指令和dragEventArray方法重复绑定拖拽事件
+ // 实际的拖拽逻辑由dragEventArray方法处理
+ el.style.position = 'absolute';
+ }
+ }
+ },
+ methods: {
+ // 拖拽事件初始化方法,用于处理数组类型元素的拖拽
+ dragEventArray(type) {
+ // 使用类选择器获取对应的元素,更准确高效
+ const elements = document.querySelectorAll(`.${type}`);
+
+ elements.forEach((el, index) => {
+ // 检查是否已经绑定过拖拽事件,避免重复绑定
+ if (el.getAttribute('drag-handler') === 'true') return;
+ el.setAttribute('drag-handler', 'true');
+
+ // 为每个元素添加拖拽事件
+ el.onmousedown = (event) => {
+ event.preventDefault();
+
+ // 计算鼠标按下位置与元素左上角的偏移
+ let sentX = event.clientX - el.offsetLeft;
+ let sentY = event.clientY - el.offsetTop;
+
+ // 获取父容器的边界
+ let parent = el.parentElement;
+ let l = 0;
+ let t = 0;
+ let r = parent.offsetWidth - el.offsetWidth;
+ let b = parent.offsetHeight - el.offsetHeight;
+
+ document.onmousemove = (event) => {
+ event.preventDefault();
+
+ // 计算新位置
+ let slideLeft = event.clientX - sentX;
+ let slideTop = event.clientY - sentY;
+
+ // 限制在父容器内
+ slideLeft <= l && (slideLeft = l);
+ slideLeft >= r && (slideLeft = r);
+ slideTop <= t && (slideTop = t);
+ slideTop >= b && (slideTop = b);
+
+ // 更新位置
+ if (Array.isArray(this.form[type]) && this.form[type][index]) {
+ // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
+ this.$set(this.form[type][index], 'left', slideLeft);
+ this.$set(this.form[type][index], 'top', slideTop);
+
+ // 直接更新DOM样式,确保视觉效果即时生效
+ el.style.left = slideLeft + 'px';
+ el.style.top = slideTop + 'px';
+
+ // 强制Vue更新,确保表单输入框同步更新
+ this.$forceUpdate();
+ } else { // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
+ this.$set(this.form[type], 'left', slideLeft);
+ this.$set(this.form[type], 'top', slideTop);
+ // 直接更新DOM样式,确保视觉效果即时生效
+ el.style.left = slideLeft + 'px';
+ el.style.top = slideTop + 'px';
+ // 强制Vue更新,确保表单输入框同步更新
+ this.$forceUpdate();
+ }
+ };
+
+ document.onmouseup = () => {
+ document.onmousemove = null;
+ document.onmouseup = null;
+ };
+
+ return false;
+ };
+ });
+ },
+
+ // 加载数据
+ loadData() {
+ let self = this;
+ self.loading = true;
+ BusinessApi.templateDefault({}, true)
+ .then(res => {
+ if (res.data.data) {
+ try {
+ const data = JSON.parse(res.data.data);
+ self.form = data;
+ } catch (e) {
+ console.error('解析数据失败', e);
+ }
+ }
+ self.loading = false;
+ })
+ .catch(error => {
+ self.loading = false;
+ console.error('加载数据失败', error);
+ });
+ },
+
+ // 拖动处理
+ dragDiv(x, y, type, index) {
+ // 确保坐标值有效
+ x = Math.max(0, x || 0);
+ y = Math.max(0, y || 0);
+
+ // 确保form对象存在
+ if (!this.form) {
+ console.warn('Form object not initialized');
+ return;
+ }
+
+ switch (type) {
+ case 'avatar':
+ if (this.form.avatar) {
+ this.form.avatar.left = x;
+ this.form.avatar.top = y;
+ }
+ break;
+ case 'logo':
+ if (this.form.logo) {
+ this.form.logo.left = x;
+ this.form.logo.top = y;
+ }
+ break;
+ case 'name':
+ if (this.form.name) {
+ this.form.name.left = x;
+ this.form.name.top = y;
+ }
+ break;
+ case 'mobile':
+ if (this.form.mobile) {
+ this.form.mobile.left = x;
+ this.form.mobile.top = y;
+ }
+ break;
+ case 'wechat':
+ if (this.form.wechat) {
+ this.form.wechat.left = x;
+ this.form.wechat.top = y;
+ }
+ break;
+ case 'mailbox':
+ if (this.form.mailbox) {
+ this.form.mailbox.left = x;
+ this.form.mailbox.top = y;
+ }
+ break;
+ case 'phone':
+ if (this.form.phone) {
+ this.form.phone.left = x;
+ this.form.phone.top = y;
+ }
+ break;
+ case 'unit':
+ // 增强公司元素的拖拽处理
+ if (Array.isArray(this.form.unit) && this.form.unit[index]) {
+ // 确保对象存在并具有left和top属性
+ if (!this.form.unit[index].left && this.form.unit[index].left !== 0) {
+ this.form.unit[index].left = 0;
+ }
+ if (!this.form.unit[index].top && this.form.unit[index].top !== 0) {
+ this.form.unit[index].top = 0;
+ }
+ this.form.unit[index].left = x;
+ this.form.unit[index].top = y;
+ } else {
+ console.warn('Unit element not found at index:', index);
+ }
+ break;
+ case 'duties':
+ // 增强职位元素的拖拽处理
+ if (Array.isArray(this.form.duties) && this.form.duties[index]) {
+ if (!this.form.duties[index].left && this.form.duties[index].left !== 0) {
+ this.form.duties[index].left = 0;
+ }
+ if (!this.form.duties[index].top && this.form.duties[index].top !== 0) {
+ this.form.duties[index].top = 0;
+ }
+ this.form.duties[index].left = x;
+ this.form.duties[index].top = y;
+ } else {
+ console.warn('Duties element not found at index:', index);
+ }
+ break;
+ case 'address':
+ // 增强地址元素的拖拽处理
+ if (Array.isArray(this.form.address) && this.form.address[index]) {
+ if (!this.form.address[index].left && this.form.address[index].left !== 0) {
+ this.form.address[index].left = 0;
+ }
+ if (!this.form.address[index].top && this.form.address[index].top !== 0) {
+ this.form.address[index].top = 0;
+ }
+ this.form.address[index].left = x;
+ this.form.address[index].top = y;
+ } else {
+ console.warn('Address element not found at index:', index);
+ }
+ break;
+ case 'iconL':
+ if (Array.isArray(this.form.iconL) && this.form.iconL[index]) {
+ this.form.iconL[index].left = x;
+ this.form.iconL[index].top = y;
+ }
+ break;
+ default:
+ console.warn('Unknown drag type:', type);
+ }
+ },
+
+ // 打开上传
+ openUpload(type, index) {
+ this.uploadType = type;
+ this.uploadIndex = index;
+ this.isupload = true;
+ },
+
+ // 返回图片
+ returnImgsFunc(e) {
+ if (e && e.length > 0) {
+ switch (this.uploadType) {
+ case 1: // 背景图
+ this.form.backdrop.src = e[0].file_path;
+ // 这里可以添加获取图片尺寸的逻辑
+ break;
+ case 2: // Logo
+ this.form.logo.src = e[0].file_path;
+ break;
+ case 3: // 图标
+ if (!this.form.iconL[this.uploadIndex]) {
+ this.form.iconL[this.uploadIndex] = {};
+ }
+ this.form.iconL[this.uploadIndex].src = e[0].file_path;
+ break;
+ }
+ }
+ this.isupload = false;
+ },
+
+ // 添加图标
+ addIcon() {
+ this.form.iconL.push({
+ src: '',
+ width: 30,
+ height: 30,
+ left: 100,
+ top: 100
+ });// 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('iconL');
+ });
+ },
+
+ // 减少图标
+ editIcon() {
+ if (this.form.iconL.length > 0) {
+ this.form.iconL.pop();
+ }
+ },
+
+ // 添加公司
+ addUnit() {
+ const index = this.form.unit.length;
+ this.form.unit.push({
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 167 + (index * 20),
+ fontWeight: 100,
+ type: 'text'
+ });
+
+ // 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('unit');
+ });
+ },
+
+ // 减少公司
+ editUnit() {
+ if (this.form.unit.length > 0) {
+ this.form.unit.pop();
+ }
+ },
+
+ // 添加职位
+ addDuties() {
+ const index = this.form.duties.length;
+ this.form.duties.push({
+ fontSize: 14,
+ color: '#000000',
+ left: 260,
+ top: 167 + (index * 20),
+ fontWeight: 400,
+ type: 'text'
+ });
+
+ // 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('duties');
+ });
+ },
+
+ // 减少职位
+ editDuties() {
+ if (this.form.duties.length > 0) {
+ this.form.duties.pop();
+ }
+ },
+
+ // 添加地址
+ addAddress() {
+ const index = this.form.address.length;
+ this.form.address.push({
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 206 + (index * 20),
+ fontWeight: 400,
+ type: 'text'
+ });
+
+ // 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('address');
+ });
+ },
+
+ // 减少地址
+ editAddress() {
+ if (this.form.address.length > 0) {
+ this.form.address.pop();
+ }
+ },
+
+ // 提交表单
+ onSubmit() {
+ let self = this;
+ self.$refs.form.validate(valid => {
+ if (valid) {
+ self.loading = true;
+
+ // 确保必要字段存在
+ if (!self.form.backdrop.src) {
+ self.$message.error('请上传背景图');
+ self.loading = false;
+ return;
+ }
+
+ BusinessApi.templateSave({
+ template: self.form
+ }, true)
+ .then(res => {
+ self.loading = false;
+ self.$message({
+ message: '保存成功',
+ type: 'success'
+ });
+ self.$router.push('/plus/business/template/index');
+ })
+ .catch(error => {
+ self.loading = false;
+ self.$message.error('保存失败,请重试');
+ });
+ }
+ });
+ },
+
+ // 返回
+ back() {
+ this.$router.push('/plus/business/template/index');
+ }
+ }
+};
+</script>
+
+<style scoped>
+.poster-box {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.poster-box .left-box {
+ position: relative;
+ overflow: hidden;
+ margin: 0 30px;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ background-color: #f5f5f5;
+}
+
+.poster-box .left-box .img img {
+ width: auto;
+ object-fit: cover;
+}
+
+.poster-box .left-box .userinfo {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.poster-box .left-box .pa {
+ position: absolute;
+ cursor: move;
+}
+
+.poster-box .left-box .photo,
+.poster-box .left-box .logo,
+.poster-box .left-box .icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ background: #ffffff;
+ border: 1px solid #e0e0e0;
+}
+
+.poster-box .left-box .photo.radius,
+.poster-box .left-box .logo.radius,
+.poster-box .left-box .icon.radius {
+ border-radius: 50%;
+}
+
+.poster-box .left-box .photo img,
+.poster-box .left-box .logo img,
+.poster-box .left-box .icon img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.poster-box .left-box .name,
+.poster-box .left-box .mobile,
+.poster-box .left-box .unit,
+.poster-box .left-box .duties,
+.poster-box .left-box .address,
+.poster-box .left-box .wechat,
+.poster-box .left-box .mailbox,
+.poster-box .left-box .phone {
+ padding: 2px 5px;
+ white-space: nowrap;
+ border-radius: 4px;
+}
+
+.poster-box .right-box {
+ flex: 1;
+ min-width: 400px;
+ max-height: 800px;
+ overflow-y: auto;
+ padding: 0 20px;
+}
+
+.tips {
+ margin-top: 5px;
+ color: #999;
+ font-size: 12px;
+}
+
+.d-s-r {
+ display: flex;
+ align-items: center;
+}
+
+.max-w460 {
+ max-width: 460px;
+}
+
+.max-w200 {
+ max-width: 200px;
+}
+
+.mr-10 {
+ margin-right: 10px;
+}
+
+.ml-10 {
+ margin-left: 10px;
+}
+
+.icon-setting {
+ border: 1px solid #e0e0e0;
+ padding: 10px;
+ margin-bottom: 10px;
+ border-radius: 4px;
+}
+</style>
\ No newline at end of file
diff --git a/shop_vue/src/views/plus/business/template/edit.vue b/shop_vue/src/views/plus/business/template/edit.vue
new file mode 100644
index 0000000..ec14b25
--- /dev/null
+++ b/shop_vue/src/views/plus/business/template/edit.vue
@@ -0,0 +1,1014 @@
+<template>
+ <!--
+ 作者:系统自动生成
+ 时间:当前日期
+ 描述:插件中心-名片模板-编辑模板
+ -->
+ <div class="user" v-loading="loading">
+ <div class="common-form">名片模板编辑</div>
+ <div class="poster-box d-s-s">
+ <div class="left-box">
+ <div v-if="form.backdrop" class="img"><img v-img-url="form.backdrop.src" /></div>
+ <div class="userinfo">
+ <!-- 头像 -->
+ <div v-if="form.avatar.display == 1"
+ class="photo pa"
+ v-drag="{type:'avatar',obj:this}"
+ :class="{ radius: form.avatar.style == 'circle' }"
+ :style="'width:' + form.avatar.width + 'px;height:' + form.avatar.width + 'px;top:' + form.avatar.top + 'px;left:' + form.avatar.left + 'px;background-color:#f0f0f0;display:flex;align-items:center;justify-content:center;color:#999'">
+ <span>头像</span>
+ </div>
+
+ <!-- Logo -->
+ <div v-if="form.logo.display == 1"
+ class="logo pa"
+ v-drag="{type:'logo',obj:this}"
+ :class="{ radius: form.logo.style == 'circle' }"
+ :style="'width:' + form.logo.width + 'px;height:' + form.logo.height + 'px;top:' + form.logo.top + 'px;left:' + form.logo.left + 'px;'">
+ <img v-img-url="form.logo.src" alt="" />
+ </div>
+
+ <!-- 姓名 -->
+ <div class="name pa"
+ v-drag="{type:'name',obj:this}"
+ :style="'font-size:' + form.name.fontSize + 'px;color:' + form.name.color + ';top:' + form.name.top + 'px;left:' + form.name.left + 'px;'">
+ 这里是姓名
+ </div>
+
+ <!-- 手机 -->
+ <div class="mobile pa"
+ v-drag="{type:'mobile',obj:this}"
+ :style="'font-size:' + form.mobile.fontSize + 'px;color:' + form.mobile.color + ';top:' + form.mobile.top + 'px;left:' + form.mobile.left + 'px;'">
+ 手机:134xxxxxxxx
+ </div>
+
+ <!-- 公司 -->
+ <div v-for="(item, index) in form.unit" :key="'unit' + index"
+ :class="'unit' + index + ' pa unit'"
+ v-drag="{type:'unit', index:index, obj:this}"
+ :style="'font-size:' + form.unit[index].fontSize + 'px;color:' + form.unit[index].color + ';top:' + form.unit[index].top + 'px;left:' + form.unit[index].left + 'px;'">
+ 这是公司{{index+1}}
+ </div>
+
+ <!-- 职位 -->
+ <div v-for="(item, index) in form.duties" :key="'duties' + index"
+ :class="'duties' + index + ' pa duties'"
+ v-drag="{type:'duties', index:index, obj:this}"
+ :style="'font-size:' + form.duties[index].fontSize + 'px;color:' + form.duties[index].color + ';top:' + form.duties[index].top + 'px;left:' + form.duties[index].left + 'px;'">
+ 这是职位{{index+1}}
+ </div>
+
+ <!-- 地址 -->
+ <div v-for="(item, index) in form.address" :key="'address' + index"
+ :class="'address' + index + ' pa address'"
+ v-drag="{type:'address', index:index, obj:this}"
+ :style="'font-size:' + form.address[index].fontSize + 'px;color:' + form.address[index].color + ';top:' + form.address[index].top + 'px;left:' + form.address[index].left + 'px;'">
+ 地址:广西壮族自治区南宁市江南区壮锦大道八桂绿城·龙湖御景-A栋-2单元{{index+1}}号
+ </div>
+
+ <!-- 微信 -->
+ <div class="wechat pa"
+ v-drag="{type:'wechat',obj:this}"
+ :style="'font-size:' + form.wechat.fontSize + 'px;color:' + form.wechat.color + ';top:' + form.wechat.top + 'px;left:' + form.wechat.left + 'px;'">
+ 微信:134xxxxxxxx
+ </div>
+
+ <!-- 邮箱 -->
+ <div class="mailbox pa"
+ v-drag="{type:'mailbox',obj:this}"
+ :style="'font-size:' + form.mailbox.fontSize + 'px;color:' + form.mailbox.color + ';top:' + form.mailbox.top + 'px;left:' + form.mailbox.left + 'px;'">
+ 邮箱:134xxxxxxxx@.xxx.com
+ </div>
+
+ <!-- 电话 -->
+ <div class="phone pa"
+ v-drag="{type:'phone',obj:this}"
+ :style="'font-size:' + form.phone.fontSize + 'px;color:' + form.phone.color + ';top:' + form.phone.top + 'px;left:' + form.phone.left + 'px;'">
+ 电话:xxx-xxx-xxx
+ </div>
+
+ <!-- 自定义图标 -->
+ <div v-for="(item, index) in form.iconL" :key="'iconL' + index"
+ :class="'iconL' + index + ' pa icon'"
+ v-drag="{type:'iconL', index:index, obj:this}"
+ :style="'width:' + form.iconL[index].width + 'px;height:' + form.iconL[index].height + 'px;top:' + form.iconL[index].top + 'px;left:' + form.iconL[index].left + 'px;'">
+ <img v-img-url="form.iconL[index].src" alt="" />
+ </div>
+ </div>
+ </div>
+
+ <div class="right-box flex-1">
+ <el-form size="small" ref="form" :model="form" label-width="150px">
+ <!-- 背景图 -->
+ <el-form-item label="海报背景图">
+ <el-button type="primary" @click="openUpload(1)">上传图片</el-button>
+ <div class="tips">尺寸建议:宽750像素 高大于(等于)1200像素</div>
+ </el-form-item>
+
+ <!-- 头像设置 -->
+ <el-form-item label="是否显示头像">
+ <el-radio v-model="form.avatar.display" label="1">显示</el-radio>
+ <el-radio v-model="form.avatar.display" label="0">隐藏</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.avatar.display == 1" label="头像宽度" prop="avatar.width" :rules="[{ required: true, message: '请输入头像宽度' }]">
+ <el-input v-model.number="form.avatar.width" min="30" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item v-if="form.avatar.display == 1" label="头像样式">
+ <el-radio v-model="form.avatar.style" label="square">正方形</el-radio>
+ <el-radio v-model="form.avatar.style" label="circle">圆形</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.avatar.display == 1" label="头像位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.avatar.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.avatar.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- Logo设置 -->
+ <el-form-item label="是否显示Logo">
+ <el-radio v-model="form.logo.display" label="1">显示</el-radio>
+ <el-radio v-model="form.logo.display" label="0">隐藏</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo图片">
+ <el-button type="primary" @click="openUpload(2)">上传图片</el-button>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo尺寸" prop="logo.width" :rules="[{ required: true, message: '请输入Logo宽度' }]">
+ <div class="d-s-r">
+ <el-input v-model.number="form.logo.width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.logo.height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
+ </div>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo样式">
+ <el-radio v-model="form.logo.style" label="square">正方形</el-radio>
+ <el-radio v-model="form.logo.style" label="circle">圆形</el-radio>
+ </el-form-item>
+ <el-form-item v-if="form.logo.display == 1" label="Logo位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.logo.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.logo.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 姓名设置 -->
+ <el-form-item label="姓名字体大小" prop="name.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
+ <el-input v-model.number="form.name.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="姓名字体颜色">
+ <el-color-picker v-model="form.name.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="姓名位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.name.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.name.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 手机设置 -->
+ <el-form-item label="手机字体大小" prop="mobile.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
+ <el-input v-model.number="form.mobile.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="手机字体颜色">
+ <el-color-picker v-model="form.mobile.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="手机位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.mobile.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.mobile.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 图标数量设置 -->
+ <el-form-item label="图标数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editIcon" :disabled="form.iconL.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.iconL.length }}</span>
+ <el-button type="text" @click="addIcon">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 图标设置 -->
+ <div v-for="(item, index) in form.iconL" :key="'icon_setting' + index" class="icon-setting">
+ <el-form-item :label="'图标' + (index + 1)">
+ <el-button type="primary" @click="openUpload(3, index)">上传图片</el-button>
+ </el-form-item>
+ <el-form-item :label="'图标' + (index + 1) + '尺寸'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.iconL[index].width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.iconL[index].height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
+ </div>
+ </el-form-item>
+ <el-form-item :label="'图标' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.iconL[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.iconL[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 公司数量设置 -->
+ <el-form-item label="公司数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editUnit" :disabled="form.unit.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.unit.length }}</span>
+ <el-button type="text" @click="addUnit">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 公司设置 -->
+ <div v-for="(item, index) in form.unit" :key="'unit_setting' + index" class="unit-setting">
+ <el-form-item :label="'公司' + (index + 1) + '字体大小'">
+ <el-input v-model.number="form.unit[index].fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item :label="'公司' + (index + 1) + '字体颜色'">
+ <el-color-picker v-model="form.unit[index].color"></el-color-picker>
+ </el-form-item>
+ <el-form-item :label="'公司' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.unit[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.unit[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 职位数量设置 -->
+ <el-form-item label="职位数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editDuties" :disabled="form.duties.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.duties.length }}</span>
+ <el-button type="text" @click="addDuties">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 职位设置 -->
+ <div v-for="(item, index) in form.duties" :key="'duties_setting' + index" class="duties-setting">
+ <el-form-item :label="'职位' + (index + 1) + '字体大小'">
+ <el-input v-model.number="form.duties[index].fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item :label="'职位' + (index + 1) + '字体颜色'">
+ <el-color-picker v-model="form.duties[index].color"></el-color-picker>
+ </el-form-item>
+ <el-form-item :label="'职位' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.duties[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.duties[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 地址数量设置 -->
+ <el-form-item label="地址数量">
+ <div class="d-s-r">
+ <el-button type="text" @click="editAddress" :disabled="form.address.length <= 0">
+ <i class="el-icon-minus"></i>
+ </el-button>
+ <span class="mr-10 ml-10">{{ form.address.length }}</span>
+ <el-button type="text" @click="addAddress">
+ <i class="el-icon-plus"></i>
+ </el-button>
+ </div>
+ </el-form-item>
+
+ <!-- 地址设置 -->
+ <div v-for="(item, index) in form.address" :key="'address_setting' + index" class="address-setting">
+ <el-form-item :label="'地址' + (index + 1) + '字体大小'">
+ <el-input v-model.number="form.address[index].fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item :label="'地址' + (index + 1) + '字体颜色'">
+ <el-color-picker v-model="form.address[index].color"></el-color-picker>
+ </el-form-item>
+ <el-form-item :label="'地址' + (index + 1) + '位置'">
+ <div class="d-s-r">
+ <el-input v-model.number="form.address[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.address[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+ </div>
+
+ <!-- 微信设置 -->
+ <el-form-item label="微信字体大小">
+ <el-input v-model.number="form.wechat.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="微信字体颜色">
+ <el-color-picker v-model="form.wechat.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="微信位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.wechat.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.wechat.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 邮箱设置 -->
+ <el-form-item label="邮箱字体大小">
+ <el-input v-model.number="form.mailbox.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="邮箱字体颜色">
+ <el-color-picker v-model="form.mailbox.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="邮箱位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.mailbox.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.mailbox.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 电话设置 -->
+ <el-form-item label="电话字体大小">
+ <el-input v-model.number="form.phone.fontSize" min="12" type="number" class="max-w460"></el-input>
+ </el-form-item>
+ <el-form-item label="电话字体颜色">
+ <el-color-picker v-model="form.phone.color"></el-color-picker>
+ </el-form-item>
+ <el-form-item label="电话位置">
+ <div class="d-s-r">
+ <el-input v-model.number="form.phone.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
+ <span class="mr-10">x</span>
+ <el-input v-model.number="form.phone.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
+ </div>
+ </el-form-item>
+
+ <!-- 提交按钮 -->
+ <div class="common-button-wrapper">
+ <el-button @click="back">返回</el-button>
+ <el-button type="primary" @click="onSubmit" :loading="loading">提交</el-button>
+ </div>
+ </el-form>
+ </div>
+ </div>
+
+ <!-- 上传图片组件 -->
+ <Upload v-if="isupload" :isupload="isupload" :type="uploadType" :index="uploadIndex" @returnImgs="returnImgsFunc">上传图片</Upload>
+ </div>
+</template>
+
+<script>
+import Upload from '@/components/file/Upload';
+import BusinessApi from '@/api/business';
+
+export default {
+ components: {
+ Upload
+ },
+ data() {
+ return {
+ loading: false,
+ isupload: false,
+ uploadType: 1,
+ uploadIndex: 0,
+ form: {
+ backdrop: {
+ src: '',
+ height: 0,
+ width: 0,
+ type: 'backdrop'
+ },
+ is_business: 0,
+ name: {
+ fontSize: 14,
+ color: '#000000',
+ left: 232,
+ top: 13,
+ fontWeight: 400,
+ type: 'text'
+ },
+ avatar: {
+ width: 70,
+ style: 'circle',
+ left: 37,
+ top: 37,
+ display: 1,
+ src: '',
+ type: 'avatar'
+ },
+ logo: {
+ width: 70,
+ height: 27,
+ style: 'square',
+ left: 22,
+ src: '',
+ top: 24,
+ display: 1,
+ type: 'image'
+ },
+ mobile: {
+ fontSize: 14,
+ color: '#000000',
+ left: 192,
+ top: 43,
+ fontWeight: 400
+ },
+ address: [
+ {
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 206,
+ fontWeight: 400,
+ type: 'text'
+ }
+ ],
+ unit: [
+ {
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 167,
+ fontWeight: 100,
+ type: 'text'
+ }
+ ],
+ duties: [
+ {
+ fontSize: 14,
+ color: '#000000',
+ left: 260,
+ top: 167,
+ fontWeight: 400,
+ type: 'text'
+ }
+ ],
+ position: [],
+ wechat: {
+ fontSize: 14,
+ color: '#000000',
+ left: 205,
+ top: 65,
+ fontWeight: 400,
+ type: 'text'
+ },
+ mailbox: {
+ fontSize: 14,
+ color: '#000000',
+ left: 205,
+ top: 104,
+ fontWeight: 400,
+ type: 'text'
+ },
+ phone: {
+ fontSize: 14,
+ color: '#000000',
+ left: 205,
+ top: 84,
+ fontWeight: 400,
+ type: 'text'
+ },
+ positionNum: 0,
+ iconL: []
+ }
+ };
+ },
+ directives: {
+ drag: {
+ inserted(el, binding) {
+ const { type, obj } = binding.value;
+
+ // 添加类型相关的类名,用于拖拽事件的绑定
+ el.classList.add('drag');
+ el.classList.add(type);
+
+ // 避免指令和dragEventArray方法重复绑定拖拽事件
+ // 实际的拖拽逻辑由dragEventArray方法处理
+ el.style.position = 'absolute';
+ }
+ }
+ },
+ created() {
+ // 获取URL参数中的template_id
+ this.template_id = this.$route.query.template_id;
+ if (this.template_id) {
+ this.loadData();
+ }
+ },
+ mounted() {
+ // 在组件挂载完成后,为所有可拖拽元素初始化拖拽事件
+ this.$nextTick(() => {
+ // 为数组类型的元素初始化拖拽事件
+ Object.keys(this.form).forEach(key => {
+ this.dragEventArray(key);
+ });
+ });
+ },
+ methods: {
+ // 拖拽事件初始化方法,用于处理数组类型元素的拖拽
+ dragEventArray(type) {
+ // 使用类选择器获取对应的元素,更准确高效
+ const elements = document.querySelectorAll(`.${type}`);
+
+ elements.forEach((el, index) => {
+ // 检查是否已经绑定过拖拽事件,避免重复绑定
+ if (el.getAttribute('drag-handler') === 'true') return;
+ el.setAttribute('drag-handler', 'true');
+
+ // 为每个元素添加拖拽事件
+ el.onmousedown = (event) => {
+ event.preventDefault();
+
+ // 计算鼠标按下位置与元素左上角的偏移
+ let sentX = event.clientX - el.offsetLeft;
+ let sentY = event.clientY - el.offsetTop;
+
+ // 获取父容器的边界
+ let parent = el.parentElement;
+ let l = 0;
+ let t = 0;
+ let r = parent.offsetWidth - el.offsetWidth;
+ let b = parent.offsetHeight - el.offsetHeight;
+
+ document.onmousemove = (event) => {
+ event.preventDefault();
+
+ // 计算新位置
+ let slideLeft = event.clientX - sentX;
+ let slideTop = event.clientY - sentY;
+
+ // 限制在父容器内
+ slideLeft <= l && (slideLeft = l);
+ slideLeft >= r && (slideLeft = r);
+ slideTop <= t && (slideTop = t);
+ slideTop >= b && (slideTop = b);
+
+ // 更新位置
+ if (Array.isArray(this.form[type]) && this.form[type][index]) {
+ // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
+ this.$set(this.form[type][index], 'left', slideLeft);
+ this.$set(this.form[type][index], 'top', slideTop);
+
+ // 直接更新DOM样式,确保视觉效果即时生效
+ el.style.left = slideLeft + 'px';
+ el.style.top = slideTop + 'px';
+
+ // 强制Vue更新,确保表单输入框同步更新
+ this.$forceUpdate();
+ } else {
+ // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
+ this.$set(this.form[type], 'left', slideLeft);
+ this.$set(this.form[type], 'top', slideTop);
+ // 直接更新DOM样式,确保视觉效果即时生效
+ el.style.left = slideLeft + 'px';
+ el.style.top = slideTop + 'px';
+ // 强制Vue更新,确保表单输入框同步更新
+ this.$forceUpdate();
+ }
+ };
+
+ document.onmouseup = () => {
+ document.onmousemove = null;
+ document.onmouseup = null;
+ };
+
+ return false;
+ };
+ });
+ },
+
+ // 加载数据
+ loadData() {
+ let self = this;
+ self.loading = true;
+ BusinessApi.templateDetail({template_id: self.template_id}, true)
+ .then(res => {
+ if (res.data.data) {
+ try {
+ const data = JSON.parse(res.data.data);
+ // 确保所有必要的数组字段存在
+ if (!data.unit) data.unit = [];
+ if (!data.duties) data.duties = [];
+ if (!data.address) data.address = [];
+ if (!data.iconL) data.iconL = [];
+ self.form = data;
+
+ // 数据加载完成后,重新初始化拖拽事件
+ self.$nextTick(() => {
+ ['unit', 'duties', 'address', 'iconL'].forEach(type => {
+ self.dragEventArray(type);
+ });
+ });
+ } catch (e) {
+ console.error('解析数据失败', e);
+ }
+ }
+ self.loading = false;
+ })
+ .catch(error => {
+ self.loading = false;
+ console.error('加载数据失败', error);
+ });
+ },
+
+ // 拖动处理
+ dragDiv(x, y, type, index) {
+ // 确保坐标值有效
+ x = Math.max(0, x || 0);
+ y = Math.max(0, y || 0);
+
+ // 确保form对象存在
+ if (!this.form) {
+ console.warn('Form object not initialized');
+ return;
+ }
+
+ switch (type) {
+ case 'avatar':
+ if (this.form.avatar) {
+ this.form.avatar.left = x;
+ this.form.avatar.top = y;
+ }
+ break;
+ case 'logo':
+ if (this.form.logo) {
+ this.form.logo.left = x;
+ this.form.logo.top = y;
+ }
+ break;
+ case 'name':
+ if (this.form.name) {
+ this.form.name.left = x;
+ this.form.name.top = y;
+ }
+ break;
+ case 'mobile':
+ if (this.form.mobile) {
+ this.form.mobile.left = x;
+ this.form.mobile.top = y;
+ }
+ break;
+ case 'wechat':
+ if (this.form.wechat) {
+ this.form.wechat.left = x;
+ this.form.wechat.top = y;
+ }
+ break;
+ case 'mailbox':
+ if (this.form.mailbox) {
+ this.form.mailbox.left = x;
+ this.form.mailbox.top = y;
+ }
+ break;
+ case 'phone':
+ if (this.form.phone) {
+ this.form.phone.left = x;
+ this.form.phone.top = y;
+ }
+ break;
+ case 'unit':
+ // 增强公司元素的拖拽处理
+ if (Array.isArray(this.form.unit) && this.form.unit[index]) {
+ // 确保对象存在并具有left和top属性
+ if (!this.form.unit[index].left && this.form.unit[index].left !== 0) {
+ this.form.unit[index].left = 0;
+ }
+ if (!this.form.unit[index].top && this.form.unit[index].top !== 0) {
+ this.form.unit[index].top = 0;
+ }
+ this.form.unit[index].left = x;
+ this.form.unit[index].top = y;
+ } else {
+ console.warn('Unit element not found at index:', index);
+ }
+ break;
+ case 'duties':
+ // 增强职位元素的拖拽处理
+ if (Array.isArray(this.form.duties) && this.form.duties[index]) {
+ if (!this.form.duties[index].left && this.form.duties[index].left !== 0) {
+ this.form.duties[index].left = 0;
+ }
+ if (!this.form.duties[index].top && this.form.duties[index].top !== 0) {
+ this.form.duties[index].top = 0;
+ }
+ this.form.duties[index].left = x;
+ this.form.duties[index].top = y;
+ } else {
+ console.warn('Duties element not found at index:', index);
+ }
+ break;
+ case 'address':
+ // 增强地址元素的拖拽处理
+ if (Array.isArray(this.form.address) && this.form.address[index]) {
+ if (!this.form.address[index].left && this.form.address[index].left !== 0) {
+ this.form.address[index].left = 0;
+ }
+ if (!this.form.address[index].top && this.form.address[index].top !== 0) {
+ this.form.address[index].top = 0;
+ }
+ this.form.address[index].left = x;
+ this.form.address[index].top = y;
+ } else {
+ console.warn('Address element not found at index:', index);
+ }
+ break;
+ case 'iconL':
+ if (Array.isArray(this.form.iconL) && this.form.iconL[index]) {
+ this.form.iconL[index].left = x;
+ this.form.iconL[index].top = y;
+ }
+ break;
+ default:
+ console.warn('Unknown drag type:', type);
+ }
+ },
+
+ // 打开上传
+ openUpload(type, index) {
+ this.uploadType = type;
+ this.uploadIndex = index;
+ this.isupload = true;
+ },
+
+ // 返回图片
+ returnImgsFunc(e) {
+ if (e && e.length > 0) {
+ switch (this.uploadType) {
+ case 1: // 背景图
+ this.form.backdrop.src = e[0].file_path;
+ // 这里可以添加获取图片尺寸的逻辑
+ break;
+ case 2: // Logo
+ this.form.logo.src = e[0].file_path;
+ break;
+ case 3: // 图标
+ if (!this.form.iconL[this.uploadIndex]) {
+ this.form.iconL[this.uploadIndex] = {};
+ }
+ this.form.iconL[this.uploadIndex].src = e[0].file_path;
+ break;
+ }
+ }
+ this.isupload = false;
+ },
+
+ // 添加图标
+ addIcon() {
+ this.form.iconL.push({
+ src: '',
+ width: 30,
+ height: 30,
+ left: 100,
+ top: 100
+ });
+ // 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('iconL');
+ });
+ },
+
+ // 减少图标
+ editIcon() {
+ if (this.form.iconL.length > 0) {
+ this.form.iconL.pop();
+ }
+ },
+
+ // 添加公司
+ addUnit() {
+ const index = this.form.unit.length;
+ this.form.unit.push({
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 167 + (index * 20),
+ fontWeight: 100,
+ type: 'text'
+ });
+
+ // 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('unit');
+ });
+ },
+
+ // 减少公司
+ editUnit() {
+ if (this.form.unit.length > 0) {
+ this.form.unit.pop();
+ }
+ },
+
+ // 添加职位
+ addDuties() {
+ const index = this.form.duties.length;
+ this.form.duties.push({
+ fontSize: 14,
+ color: '#000000',
+ left: 260,
+ top: 167 + (index * 20),
+ fontWeight: 400,
+ type: 'text'
+ });
+
+ // 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('duties');
+ });
+ },
+
+ // 减少职位
+ editDuties() {
+ if (this.form.duties.length > 0) {
+ this.form.duties.pop();
+ }
+ },
+
+ // 添加地址
+ addAddress() {
+ const index = this.form.address.length;
+ this.form.address.push({
+ fontSize: 14,
+ color: '#000000',
+ left: 133,
+ top: 206 + (index * 20),
+ fontWeight: 400,
+ type: 'text'
+ });
+
+ // 添加后重新初始化拖拽事件
+ this.$nextTick(() => {
+ this.dragEventArray('address');
+ });
+ },
+
+ // 减少地址
+ editAddress() {
+ if (this.form.address.length > 0) {
+ this.form.address.pop();
+ }
+ },
+
+ // 提交表单
+ onSubmit() {
+ let self = this;
+ self.$refs.form.validate(valid => {
+ if (valid) {
+ self.loading = true;
+
+ // 确保必要字段存在
+ if (!self.form.backdrop.src) {
+ self.$message.error('请上传背景图');
+ self.loading = false;
+ return;
+ }
+
+ BusinessApi.templateEdit({
+ template_id: self.template_id,
+ template: self.form
+ }, true)
+ .then(res => {
+ self.loading = false;
+ self.$message({
+ message: '保存成功',
+ type: 'success'
+ });
+ self.$router.push('/plus/business/template/index');
+ })
+ .catch(error => {
+ self.loading = false;
+ self.$message.error('保存失败,请重试');
+ });
+ }
+ });
+ },
+
+ // 返回
+ back() {
+ this.$router.push('/plus/business/template/index');
+ }
+ }
+};
+</script>
+
+<style scoped>
+.poster-box {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.poster-box .left-box {
+ position: relative;
+ overflow: hidden;
+ margin: 0 30px;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ background-color: #f5f5f5;
+}
+
+.poster-box .left-box .img img {
+ width: auto;
+ object-fit: cover;
+}
+
+.poster-box .left-box .userinfo {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.poster-box .left-box .pa {
+ position: absolute;
+ cursor: move;
+}
+
+.poster-box .left-box .photo,
+.poster-box .left-box .logo,
+.poster-box .left-box .icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ background: #ffffff;
+ border: 1px solid #e0e0e0;
+}
+
+.poster-box .left-box .photo.radius,
+.poster-box .left-box .logo.radius,
+.poster-box .left-box .icon.radius {
+ border-radius: 50%;
+}
+
+.poster-box .left-box .photo img,
+.poster-box .left-box .logo img,
+.poster-box .left-box .icon img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.poster-box .left-box .name,
+.poster-box .left-box .mobile,
+.poster-box .left-box .unit,
+.poster-box .left-box .duties,
+.poster-box .left-box .address,
+.poster-box .left-box .wechat,
+.poster-box .left-box .mailbox,
+.poster-box .left-box .phone {
+ padding: 2px 5px;
+ white-space: nowrap;
+ border-radius: 4px;
+}
+
+.poster-box .right-box {
+ flex: 1;
+ min-width: 400px;
+ max-height: 800px;
+ overflow-y: auto;
+ padding: 0 20px;
+}
+
+.tips {
+ margin-top: 5px;
+ color: #999;
+ font-size: 12px;
+}
+
+.d-s-r {
+ display: flex;
+ align-items: center;
+}
+
+.max-w460 {
+ max-width: 460px;
+}
+
+.max-w200 {
+ max-width: 200px;
+}
+
+.mr-10 {
+ margin-right: 10px;
+}
+
+.ml-10 {
+ margin-left: 10px;
+}
+
+.icon-setting {
+ border: 1px solid #e0e0e0;
+ padding: 10px;
+ margin-bottom: 10px;
+ border-radius: 4px;
+}
+</style>
\ No newline at end of file
diff --git a/shop_vue/src/views/plus/business/template/index.vue b/shop_vue/src/views/plus/business/template/index.vue
new file mode 100644
index 0000000..dbb58d8
--- /dev/null
+++ b/shop_vue/src/views/plus/business/template/index.vue
@@ -0,0 +1,234 @@
+<template>
+ <!--
+ 作者:系统自动生成
+ 时间:当前日期
+ 描述:插件中心-名片模板-管理列表
+ -->
+ <div class="user" v-loading="listLoading">
+ <div class="common-form">名片模板管理</div>
+ <div class="common-main">
+ <div class="common-toolbar">
+ <el-button type="primary" @click="addTemplate">添加模板</el-button>
+ <el-button type="danger" @click="batchDelete" :disabled="multipleSelection.length === 0">批量删除</el-button>
+ </div>
+
+ <el-table :data="listData" border class="common-table" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center"></el-table-column>
+ <el-table-column prop="template_id" label="ID" width="80" align="center"></el-table-column>
+ <el-table-column prop="image" label="模板图片" align="center">
+ <template slot-scope="scope">
+ <a :href="scope.row.image" target="_blank">
+ <img :src="scope.row.image" class="logo-img" alt="模板图片">
+ </a>
+ </template>
+ </el-table-column>
+ <el-table-column prop="create_time" label="创建时间" width="180" align="center"></el-table-column>
+ <el-table-column label="操作" align="center" fixed="right">
+ <template slot-scope="scope">
+ <el-button type="primary" size="small" @click="editTemplate(scope.row)" class="m-r-5">编辑</el-button>
+ <el-button type="danger" size="small" @click="deleteTemplate(scope.row)" class="m-r-5">删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="common-pagination">
+ <div class="page-text">共 {{ total }} 条记录</div>
+ <el-pagination
+ layout="prev, pager, next, jumper"
+ :total="total"
+ :page-size="listQuery.pagesize"
+ :current-page="listQuery.page"
+ @current-change="pageChange"
+ @size-change="pageSizeChange"
+ ></el-pagination>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import BusinessApi from '@/api/business';
+
+export default {
+ data() {
+ return {
+ total: 0,
+ listLoading: false,
+ multipleSelection: [],
+ listQuery: {
+ page: 1,
+ pagesize: 10,
+ title: ''
+ },
+ listData: []
+ };
+ },
+ created() {
+ this.getList();
+ },
+ methods: {
+ // 获取列表数据
+ getList() {
+ const that = this;
+ that.listLoading = true;
+ BusinessApi.templateList(that.listQuery)
+ .then(res => {
+ that.listLoading = false;
+ that.listData = res.data.list.data;
+ that.total = res.data.list.total;
+ })
+ .catch(error => {
+ that.listLoading = false;
+ console.error('获取列表失败', error);
+ that.$message.error('获取数据失败,请重试');
+ });
+ },
+ // 查询
+ handleQuery() {
+ this.listQuery.page = 1;
+ this.getList();
+ },
+ // 重置
+ resetQuery() {
+ this.listQuery = {
+ page: 1,
+ pagesize: 10,
+ title: ''
+ };
+ this.getList();
+ },
+ // 分页改变
+ pageChange(page) {
+ this.listQuery.page = page;
+ this.getList();
+ },
+ // 每页条数改变
+ pageSizeChange(pagesize) {
+ this.listQuery.pagesize = pagesize;
+ this.getList();
+ },
+ // 选择项变化
+ handleSelectionChange(selection) {
+ this.multipleSelection = selection;
+ },
+ // 批量删除
+ batchDelete() {
+ if (this.multipleSelection.length === 0) {
+ this.$message.warning('请选择要删除的模板');
+ return;
+ }
+
+ const template_ids = this.multipleSelection.map(item => item.template_id).join(',');
+
+ this.$confirm('确定要删除选中的 ' + this.multipleSelection.length + ' 个模板吗?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ }).then(() => {
+ this.listLoading = true;
+ BusinessApi.templateDelete({template_id: template_ids})
+ .then(res => {
+ this.listLoading = false;
+ this.$message({
+ message: '删除成功',
+ type: 'success'
+ });
+ this.getList();
+ this.multipleSelection = [];
+ })
+ .catch(error => {
+ this.listLoading = false;
+ this.$message.error('删除失败,请重试');
+ });
+ });
+ },
+ // 添加模板
+ addTemplate() {
+ this.$router.push('/plus/business/template/add');
+ },
+ // 编辑模板
+ editTemplate(row) {
+ this.$router.push(`/plus/business/template/edit?template_id=${row.template_id}`);
+ },
+ // 删除模板
+ deleteTemplate(row) {
+ this.$confirm('确定要删除该模板吗?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ }).then(() => {
+ this.listLoading = true;
+ BusinessApi.templateDelete({template_id: row.template_id})
+ .then(res => {
+ this.listLoading = false;
+ this.$message({
+ message: '删除成功',
+ type: 'success'
+ });
+ this.getList();
+ })
+ .catch(error => {
+ this.listLoading = false;
+ this.$message.error('删除失败,请重试');
+ });
+ });
+ },
+ // 更改默认状态
+ changeStatus(row) {
+ BusinessApi.templateDefault({template_id: row.template_id, is_default: row.is_default})
+ .then(res => {
+ this.$message({
+ message: '设置成功',
+ type: 'success'
+ });
+ })
+ .catch(error => {
+ this.$message.error('设置失败,请重试');
+ this.getList(); // 刷新列表以恢复正确状态
+ });
+ }
+ }
+};
+</script>
+
+<style scoped>
+.logo-img {
+ height: 170px;
+ border-radius: 4px;
+}
+
+.common-search {
+ margin-bottom: 15px;
+ padding: 10px 15px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+}
+
+.common-toolbar {
+ margin-bottom: 15px;
+ padding: 10px 0;
+}
+
+.common-table {
+ margin-bottom: 15px;
+}
+
+.common-pagination {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 0;
+}
+
+.page-text {
+ color: #606266;
+}
+
+.inline-input {
+ margin-right: 10px;
+}
+
+.m-r-5 {
+ margin-right: 5px;
+}
+</style>
\ No newline at end of file
--
Gitblit v1.9.2