feat: 【工作报告】功能

This commit is contained in:
韦荣超 2022-01-14 15:55:26 +08:00
parent 3602acd187
commit d48ed18102
32 changed files with 2890 additions and 3 deletions

View File

@ -0,0 +1,379 @@
<?php
namespace App\Http\Controllers\Api;
use App\Exceptions\ApiException;
use App\Models\ProjectTask;
use App\Models\Report;
use App\Models\ReportReceive;
use App\Models\User;
use App\Module\Base;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Validation\Rule;
use Request;
use Illuminate\Support\Facades\Validator;
/**
* @apiDefine report
*
* 汇报
*/
class ReportController extends AbstractController
{
/**
* @api {get} api/report/my 01. 我发送的汇报
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName my
*
* @apiParam {Number} [user] 会员ID
* @apiParam {String} [type] 汇报类型weekly:周报daily:日报
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function my(): array
{
$user = User::auth();
// 搜索当前用户
$builder = Report::query()->whereUserid($user->userid);
$type = trim(Request::input('type'));
$createAt = Request::input('created_at');
in_array($type, [Report::WEEKLY, Report::DAILY]) && $builder->whereType($type);
$whereArray = [];
if (is_array($createAt)) {
if ($createAt[0] > 0) $whereArray[] = ['created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($createAt[0]))];
if ($createAt[1] > 0) $whereArray[] = ['created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($createAt[1]))];
}
$list = $builder->where($whereArray)->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
if ($list->items()) {
foreach ($list->items() as $item) {
$item->receivesUser;
$item->receives = empty($item->receivesUser) ? [] : array_column($item->receivesUser->toArray(), "userid");
}
}
return Base::retSuccess('success', $list);
}
/**
* 我接收的汇报
* @return array
*/
public function receive(): array
{
$user = User::auth();
$builder = Report::query();
$builder->whereHas("receivesUser", function ($query) use ($user) {
$query->where("report_receives.userid", $user->userid);
});
$type = trim(Request::input('type'));
$createAt = Request::input('created_at');
$username = trim(Request::input('username', ''));
$builder->whereHas('sendUser', function ($query) use ($username) {
if (!empty($username)) {
$query->where('users.email', 'LIKE', '%' . $username . '%');
}
});
in_array($type, [Report::WEEKLY, Report::DAILY]) && $builder->whereType($type);
$whereArray = [];
if (is_array($createAt)) {
if ($createAt[0] > 0) $whereArray[] = ['created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($createAt[0]))];
if ($createAt[1] > 0) $whereArray[] = ['created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($createAt[1]))];
}
$list = $builder->where($whereArray)->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
if ($list->items()) {
foreach ($list->items() as $item) {
$item["receive_time"] = ReportReceive::query()->whereRid($item["id"])->whereUserid($user->userid)->value("receive_time");
$item->receivesUser;
$item->receives = empty($item->receivesUser) ? [] : array_column($item->receivesUser->toArray(), "userid");
}
}
return Base::retSuccess('success', $list);
}
/**
* 保存并发送工作汇报
* @return array
*/
public function store(): array
{
$input = [
"id" => Base::getPostValue("id", 0),
"title" => Base::getPostValue("title"),
"type" => Base::getPostValue("type"),
"content" => Base::getPostValue("content"),
"receive" => Base::getPostValue("receive"),
// 以当前日期为基础的周期偏移量。例如选择了上一周那么就是 -1上一天同理。
"offset" => Base::getPostValue("offset", 0),
];
$validator = Validator::make($input, [
'id' => 'numeric',
'title' => 'required',
'type' => ['required', Rule::in([Report::WEEKLY, Report::DAILY])],
'content' => 'required',
'receive' => 'required',
'offset' => ['numeric', 'max:0'],
], [
'id.numeric' => 'ID只能是数字',
'title.required' => '请填写标题',
'type.required' => '请选择汇报类型',
'type.in' => '汇报类型错误',
'content.required' => '请填写汇报内容',
'receive.required' => '请选择接收人',
'offset.numeric' => '工作汇报周期格式错误,只能是数字',
'offset.max' => '只能提交当天/本周或者之前的的工作汇报',
]);
if ($validator->fails())
return Base::retError($validator->errors()->first());
$user = User::auth();
// 接收人
if ( is_array($input["receive"]) ) {
// 删除当前登录人
$input["receive"] = array_diff($input["receive"], [$user->userid]);
// 查询用户是否存在
if ( count($input["receive"]) !== User::whereIn("userid", $input["receive"])->count() )
return Base::retError("用户不存在");
foreach ($input["receive"] as $userid) {
$input["receive_content"][] = [
"receive_time" => Carbon::now()->toDateTimeString(),
"userid" => $userid,
"read" => 0,
];
}
}
// 在事务中运行
Report::transaction( function () use ($input, $user) {
$id = $input["id"];
if ($id) {
// 编辑
$report = Report::getOne($id);
$report->updateInstance([
"title" => $input["title"],
"type" => $input["type"],
"content" => htmlspecialchars($input["content"]),
]);
} else {
// 生成唯一标识
$sign = Report::generateSign($input["type"], $input["offset"]);
// 检查唯一标识是否存在
if (empty($input["id"])) {
if (Report::query()->whereSign($sign)->count() > 0)
throw new ApiException("请勿重复提交工作汇报");
}
$report = Report::createInstance([
"title" => $input["title"],
"type" => $input["type"],
"content" => htmlspecialchars($input["content"]),
"userid" => $user->userid,
"sign" => $sign,
]);
}
$report->save();
if (!empty($input["receive_content"])) {
// 删除关联
$report->Receives()->delete();
// 保存接收人
$report->Receives()->createMany($input["receive_content"]);
}
} );
return Base::retSuccess('success');
}
/**
* 生成汇报模板
* @return array
*/
public function template(): array
{
$user = User::auth();
$type = trim( Request::input("type") );
$offset = abs( intval( Request::input("offset", 0) ) );
$now_dt = trim( Request::input("date") ) ? Carbon::parse( Request::input("date") ) : Carbon::now();
// 获取开始时间
if ($type === Report::DAILY) {
$start_time = Carbon::today();
if ( $offset > 0 ) {
// 将当前时间调整为偏移量当天结束
$now_dt->subDays( $offset )->endOfDay();
// 开始时间偏移量计算
$start_time->subDays( $offset );
}
$end_time = Carbon::instance($start_time)->endOfDay();
} else {
$start_time = Carbon::now();
if ( $offset > 0 ) {
// 将当前时间调整为偏移量当周结束
$now_dt->subWeeks( $offset )->endOfDay();
// 开始时间偏移量计算
$start_time->subWeeks( $offset );
}
$start_time->startOfWeek();
$end_time = Carbon::instance($start_time)->endOfWeek();
}
// 生成唯一标识
$sign = Report::generateSign($type, 0, Carbon::instance($start_time));
$one = Report::query()->whereSign($sign)->first();
// 如果已经提交了相关汇报
if ($one) {
return Base::retSuccess('success', [
"content" => $one->content,
"title" => $one->title,
"id" => $one->id,
]);
}
// 已完成的任务
$completeContent = "";
$complete_task = ProjectTask::query()
->whereNotNull("complete_at")
->whereBetween("complete_at", [$start_time->toDateTimeString(), $end_time->toDateTimeString()])
->whereHas("taskUser", function ($query) use ($user) {
$query->where("userid", $user->userid);
})
->orderByDesc("id")
->get();
if ($complete_task->isNotEmpty()) {
foreach ($complete_task as $task) {
$complete_at = Carbon::parse($task->complete_at);
$pre = $type == Report::WEEKLY ? ('<span>[' . Base::Lang('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$completeContent .= '<li>' . $pre . $task->name . '</li>';
}
} else {
$completeContent = '<li>&nbsp;</li>';
}
// 未完成的任务
$unfinishedContent = "";
$unfinished_task = ProjectTask::query()
->whereNull("complete_at")
->whereNotNull("start_at")
->where("end_at", "<", $end_time->toDateTimeString())
->whereHas("taskUser", function ($query) use ($user) {
$query->where("userid", $user->userid);
})
->orderByDesc("id")
->get();
if ($unfinished_task->isNotEmpty()) {
foreach ($unfinished_task as $task) {
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
$pre = ( !empty( $end_at ) && $end_at->lt($now_dt) ) ? '<span style="color:#ff0000;">[' . Base::Lang('超期') . ']</span>&nbsp;' : '';
$unfinishedContent .= '<li>' . $pre . $task->name . '</li>';
}
} else {
$unfinishedContent = '<li>&nbsp;</li>';
}
// 生成标题
if ( $type === Report::WEEKLY ) {
$title = $user->nickname . "的周报[" . $start_time->format("m/d") . "-" . $end_time->format("m/d") . "]";
$title .= "[" . $start_time->month . "月第" . $start_time->weekOfMonth . "周]";
} else {
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
}
return Base::retSuccess('success', [
"time" => $start_time->toDateTimeString(),
"complete_task" => $complete_task,
"unfinished_task" => $unfinished_task,
"content" => '<h2>' . Base::Lang('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Base::Lang('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>',
"title" => $title,
]);
}
/**
* @return array
*/
public function detail(): array
{
$id = intval( trim( Request::input("id") ) );
if (empty( $id ))
return Base::retError("缺少ID参数");
$one = Report::getOne($id);
$one["type_val"] = $one->getRawOriginal("type");
$user = User::auth();
// 标记为已读
if ( !empty( $one->receivesUser ) ) {
foreach ($one->receivesUser as $item) {
if ($item->userid === $user->userid && $item->pivot->read === 0) {
$one->receivesUser()->updateExistingPivot($user->userid, [
"read" => 1,
]);
}
}
}
return Base::retSuccess("success", $one);
}
/**
* 获取最后一次提交的接收人
* @return array
*/
public function last_submitter(): array
{
$one = Report::getLastOne();
return Base::retSuccess("success", empty( $one["receives"] ) ? [] : $one["receives"]);
}
/**
* 获取未读
* @return array
*/
public function unread(): array
{
$userid = intval( trim( Request::input("userid") ) );
$user = empty($userid) ? User::auth() : User::find($userid);
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
$query->where("userid", $user->userid)->where("read", 0);
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
return Base::retSuccess("success", $data);
}
/**
* 标记汇报已读,可批量
* @return array
*/
public function read(): array
{
$user = User::auth();
$ids = Request::input("ids");
if ( !is_array($ids) && !is_string($ids) ) {
return Base::retError("请传入正确的工作汇报Id");
}
if ( is_string($ids) ) {
$ids = explode(",", $ids);
}
$data = Report::with(["receivesUser" => function (BelongsToMany $query) use ($user) {
$query->where("report_receives.userid", $user->userid)->where("read", 0);
}])->whereIn("id", $ids)->get();
if ( $data->isNotEmpty() ) {
foreach ($data as $item) {
(!empty($item->receivesUser) && $item->receivesUser->isNotEmpty()) && $item->receivesUser()->updateExistingPivot($user->userid, [
"read" => 1,
]);
}
}
return Base::retSuccess("success", $data);
}
}

View File

@ -44,5 +44,8 @@ class VerifyCsrfToken extends Middleware
// 保存文件内容(上传)
'api/file/content/upload/',
// 保存汇报
'api/report/store/',
];
}

156
app/Models/Report.php Normal file
View File

@ -0,0 +1,156 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use Carbon\Carbon;
use Carbon\Traits\Creator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use JetBrains\PhpStorm\Pure;
/**
* App\Models\Report
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string $title 标题
* @property string $type 汇报类型
* @property int $userid
* @property string|null $content
* @method static Builder|Report newModelQuery()
* @method static Builder|Report newQuery()
* @method static Builder|Report query()
* @method static Builder|Report whereContent($value)
* @method static Builder|Report whereCreatedAt($value)
* @method static Builder|Report whereId($value)
* @method static Builder|Report whereTitle($value)
* @method static Builder|Report whereType($value)
* @method static Builder|Report whereUpdatedAt($value)
* @method static Builder|Report whereUserid($value)
* @mixin \Eloquent
* @property string $sign 汇报唯一标识
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ReportReceive[] $Receives
* @property-read int|null $receives_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $receivesUser
* @property-read int|null $receives_user_count
* @method static Builder|Report whereSign($value)
*/
class Report extends AbstractModel
{
use HasFactory;
const WEEKLY = "weekly";
const DAILY = "daily";
protected $fillable = [
"title",
"type",
"userid",
"content",
];
protected $appends = [
'receives',
];
public function Receives(): HasMany
{
return $this->hasMany(ReportReceive::class, "rid");
}
public function receivesUser(): BelongsToMany
{
return $this->belongsToMany(User::class, ReportReceive::class, "rid", "userid")
->withPivot("receive_time", "read");
}
public function sendUser()
{
return $this->hasOne(User::class, "userid", "userid");
}
public function getTypeAttribute($value): string
{
return match ($value) {
Report::WEEKLY => "周报",
Report::DAILY => "日报",
default => "",
};
}
public function getContentAttribute($value): string
{
return htmlspecialchars_decode($value);
}
public function getReceivesAttribute()
{
if (!isset($this->appendattrs['receives'])) {
$this->appendattrs['receives'] = empty( $this->receivesUser ) ? [] : array_column($this->receivesUser->toArray(), "userid");
}
return $this->appendattrs['receives'];
}
/**
* 获取单条记录
* @param $id
* @param User|null $user
* @return Report|Builder|Model|object|null
* @throw ApiException
*/
public static function getOne($id, User $user = null)
{
$user === null && $user = User::auth();
$one = self::whereUserid($user->userid)->whereId($id)->first();
if ( empty($one) )
throw new ApiException("记录不存在");
return $one;
}
/**
* 获取最后一条提交记录
* @param User|null $user
* @return Builder|Model|\Illuminate\Database\Query\Builder|object
*/
public static function getLastOne(User $user = null)
{
$user === null && $user = User::auth();
$one = self::whereUserid($user->userid)->orderByDesc("created_at")->first();
if ( empty($one) )
throw new ApiException("记录不存在");
return $one;
}
/**
* 生成唯一标识
* @param $type
* @param $offset
* @param Carbon|null $time
* @return string
*/
public static function generateSign($type, $offset, Carbon $time = null): string
{
$user = User::auth();
$now_dt = $time === null ? Carbon::now() : $time;
$time_s = match ($type) {
Report::WEEKLY => function() use ($now_dt, $offset) {
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
$now_dt->startOfWeek(); // 设置为当周第一天
return $now_dt->year . $now_dt->weekOfYear . $now_dt->month . $now_dt->weekOfMonth;
},
Report::DAILY => function() use ($now_dt, $offset) {
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
return $now_dt->year . $now_dt->dayOfYear . $now_dt->month . $now_dt->daysInMonth;
},
default => "",
};
return md5( $user->userid . ( is_callable($time_s) ? $time_s() : "" ) . $type );
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* App\Models\ReportReceive
*
* @property int $id
* @property int $rid
* @property string|null $receive_time 接收时间
* @property int $userid 接收人
* @property int $read 是否已读
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive query()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRead($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereReceiveTime($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereUserid($value)
* @mixin \Eloquent
*/
class ReportReceive extends AbstractModel
{
use HasFactory;
// 关闭时间戳自动写入
public $timestamps = false;
protected $fillable = [
"rid",
"receive_time",
"userid",
"read",
];
}

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateReportsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( Schema::hasTable('reports') )
return;
Schema::create('reports', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string("title")->default("")->comment("标题");
$table->enum("type", ["weekly", "daily"])->default("daily")->comment("汇报类型");
$table->unsignedBigInteger("userid")->default(0);
$table->longText("content")->nullable();
$table->index(["userid", "created_at"], "default");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('reports');
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateReportReceivesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( Schema::hasTable('report_receives') )
return;
Schema::create('report_receives', function (Blueprint $table) {
$table->bigIncrements("id");
$table->unsignedInteger("rid")->default(0);
$table->timestamp("receive_time")->nullable()->comment("接收时间");
$table->unsignedBigInteger("userid")->default(0)->comment("接收人");
$table->unsignedTinyInteger("read")->default(0)->comment("是否已读");
$table->index(["userid", "receive_time"], "default");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('report_receives');
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddReportSign extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('reports', function (Blueprint $table) {
$table->string("sign")->default("")->comment("汇报唯一标识");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('reports', function (Blueprint $table) {
$table->dropColumn("sign");
});
}
}

View File

@ -21,7 +21,10 @@
v-for="(item, key) in menu"
:key="key"
:divided="!!item.divided"
:name="item.path">{{$L(item.name)}}</DropdownItem>
:name="item.path">
{{$L(item.name)}}
<Badge v-if="item.path === 'workReport'" :count="reportUnreadNumber"/>
</DropdownItem>
<Dropdown placement="right-start" @on-click="setLanguage">
<DropdownItem divided>
<div class="manage-menu-language">
@ -160,6 +163,14 @@
</div>
</Modal>
<!--工作报告-->
<DrawerOverlay
v-model="workReportShow"
placement="right"
:size="900">
<Report v-if="workReportShow" @read="reportUnread" />
</DrawerOverlay>
<!--查看所有团队-->
<DrawerOverlay
v-model="allUserShow"
@ -206,11 +217,13 @@ import ProjectManagement from "./manage/components/ProjectManagement";
import DrawerOverlay from "../components/DrawerOverlay";
import DragBallComponent from "../components/DragBallComponent";
import TaskAdd from "./manage/components/TaskAdd";
import Report from "./manage/components/Report";
import {Store} from "le5le-store";
export default {
components: {
TaskAdd,
Report,
DragBallComponent, DrawerOverlay, ProjectManagement, TeamManagement, ProjectArchived, TaskDetail},
data() {
return {
@ -242,6 +255,7 @@ export default {
show768Menu: false,
innerHeight: window.innerHeight,
workReportShow: false,
allUserShow: false,
allProjectShow: false,
archivedProjectShow: false,
@ -249,6 +263,7 @@ export default {
natificationHidden: false,
natificationReady: false,
notificationClass: null,
reportUnreadNumber: 0,
}
},
@ -273,6 +288,9 @@ export default {
if (this.$Electron) {
this.$Electron.ipcRenderer.send('setDockBadge', 0);
}
//
this.reportUnread();
},
beforeDestroy() {
@ -334,7 +352,8 @@ export default {
{path: 'clearCache', name: '清除缓存'},
{path: 'system', name: '系统设置', divided: true},
{path: 'priority', name: '任务等级'},
{path: 'allUser', name: '团队管理', divided: true},
{path: 'workReport', name: '工作报告', divided: true},
{path: 'allUser', name: '团队管理'},
{path: 'allProject', name: '所有项目'},
{path: 'archivedProject', name: '已归档的项目'}
]
@ -343,7 +362,8 @@ export default {
{path: 'personal', name: '个人设置'},
{path: 'password', name: '密码设置'},
{path: 'clearCache', name: '清除缓存'},
{path: 'archivedProject', name: '已归档的项目', divided: true}
{path: 'workReport', name: '工作报告', divided: true},
{path: 'archivedProject', name: '已归档的项目'}
]
}
},
@ -474,6 +494,9 @@ export default {
case 'archivedProject':
this.archivedProjectShow = true;
return;
case 'workReport':
this.workReportShow = true;
return;
case 'clearCache':
this.$store.dispatch("handleClearCache", null).then(() => {
$A.setStorage("clearCache", $A.randomString(6))
@ -683,6 +706,17 @@ export default {
this.natificationHidden = !!document[hiddenProperty]
}
document.addEventListener(visibilityChangeEvent, visibilityChangeListener);
},
reportUnread() {
this.$store.dispatch("call", {
url: 'report/unread',
method: 'get',
}).then(({data, msg}) => {
// data
this.reportUnreadNumber = data.total ? data.total : 0;
// msg
});
}
}
}

View File

@ -0,0 +1,65 @@
<template>
<div class="report">
<Tabs v-model="reportTabs">
<TabPane label="填写" name="edit" icon="md-create">
<ReportEdit :id="reportId" @saveSuccess="saveSuccess">填写</ReportEdit>
</TabPane>
<TabPane label="我的汇报" name="my" icon="ios-paper-plane-outline">
<ReportMy v-if="reportTabs === 'my'" @detail="showDetail" @edit="editReport">我的汇报</ReportMy>
</TabPane>
<TabPane label="收到的汇报" name="receive" icon="ios-paper-outline">
<ReportReceive v-if="reportTabs === 'receive'" @detail="showDetail">收到的汇报</ReportReceive>
</TabPane>
</Tabs>
<Drawer v-model="showDetailDrawer" width="900px" :closable="false">
<ReportDetail :data="detailData" @closeDrawer="closeDrawer"/>
</Drawer>
</div>
</template>
<script>
import ReportEdit from "./ReportEdit"
import ReportMy from "./ReportMy"
import ReportReceive from "./ReportReceive"
import ReportDetail from "./ReportDetail"
export default {
name: "Report",
components: {
ReportEdit, ReportMy, ReportReceive,ReportDetail
},
data() {
return {
reportTabs: "my",
showDetailDrawer: false,
detailData: {},
reportId: 0,
}
},
methods: {
closeDrawer(){
this.showDetailDrawer = false
},
showDetail(row) {
this.showDetailDrawer = true;
this.detailData = row;
//1.5
setTimeout(() => {
this.$emit("read");
}, 1500);
},
editReport(id) {
this.reportId = id;
this.reportTabs = "edit";
},
saveSuccess() {
this.reportId = 0;
this.reportTabs = "my";
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="report-detail">
<p class="report-title"><Icon type="ios-arrow-back" class="report-title-icon" @click="closeDrawer"/> {{ data.title }}</p>
<Divider />
<div class="report-profile">
<Row>
<Col span="2">
<div class="report-submitter"><p>{{ $L('汇报人') }} </p></div>
</Col>
<Col span="6">
<div class="report-submitter">
<UserAvatar :userid="data.userid" :size="28"/>
</div>
</Col>
<Col span="2">
<div class="report-submitter"> <p>{{ $L('提交时间') }}</p></div>
</Col>
<Col span="6">
<div class="report-submitter">
<div>{{ data.created_at }}</div>
</div>
</Col>
<Col span="2">
<div class="report-submitter"><p>{{ $L('汇报对象') }}</p></div>
</Col>
<Col span="6">
<div class="report-submitter">
<UserAvatar v-for="item in data.receives" :key="item" :userid="item" :size="28"/>
</div>
</Col>
</Row>
</div>
<Row class="report-main">
<Col span="2">
<div class="report-submitter"><p>{{ $L('汇报内容') }}</p></div>
</Col>
<Col span="22">
<div class="report-content" v-html="data.content">
</div>
</Col>
</Row>
</div>
</template>
<script>
export default {
name: "ReportDetail",
props: {
data: {
default: {},
}
},
mounted() {
if (this.data.id > 0) this.sendRead();
console.log(this.data)
},
watch: {
data() {
if (this.data.id > 0) this.sendRead();
},
},
methods: {
sendRead() {
this.$store.dispatch("call", {
url: 'report/read',
data: {ids: [this.data.id]},
method: 'get',
}).then(({data, msg}) => {
// data
// msg
}).catch(({msg}) => {
// msg
});
},
closeDrawer(){
this.$emit('closeDrawer')
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,236 @@
<template>
<Form class="report-box" label-position="top" @submit.native.prevent>
<Row class="report-row report-row-header" >
<Col span="2"><p class="report-titles">{{ $L("汇报类型") }}</p></Col>
<Col span="6">
<RadioGroup type="button" button-style="solid" v-model="reportData.type" @on-change="typeChange" class="report-radiogroup">
<Radio label="weekly">{{ $L("周报") }}</Radio>
<Radio label="daily">{{ $L("日报") }}</Radio>
</RadioGroup>
</Col>
<Col span="6">
<ButtonGroup class="report-buttongroup">
<Tooltip class="report-poptip" trigger="hover" :content="prevCycleText" placement="bottom">
<Button type="primary" @click="prevCycle">
<Icon type="ios-arrow-back" />
</Button>
</Tooltip>
<div class="report-buttongroup-shu"></div>
<Tooltip class="report-poptip" trigger="hover" :content="nextCycleText" placement="bottom">
<Button type="primary" @click="nextCycle" :disabled="reportData.offset >= 0">
<Icon type="ios-arrow-forward" />
</Button>
</Tooltip>
</ButtonGroup>
</Col>
</Row>
<Row class="report-row report-row-header">
<Col span="2"><p class="report-titles">{{ $L("汇报名称") }}</p></Col>
<Col span="22">
<Input v-model="reportData.title" disabled placeholder=""></Input>
</Col>
</Row>
<Row class="report-row report-row-header">
<Col span="2"><p class="report-titles">{{ $L("汇报对象") }}</p></Col>
<Col span="16">
<UserInput
v-if="userInputShow"
v-model="reportData.receive"
:placeholder="$L('选择接收人')" />
</Col>
<Col span="6"><a class="report-row-a" href="javascript:void(0);" @click="getLastSubmitter"><Icon class="report-row-a-icon" type="ios-share-outline" />{{ $L("使用上一次提交的接收人") }}</a></Col>
</Row>
<Row class="report-row report-row-content">
<Col span="2"><p class="report-titles">{{ $L("汇报内容") }}</p></Col>
<Col span="22">
<FormItem>
<TEditor v-model="reportData.content" height="550px"/>
</FormItem>
</Col>
</Row>
<Row class="report-row report-row-foot">
<Col span="2"></Col>
<Col span="4">
<FormItem>
<Button type="primary" @click="handleSubmit" class="report-bottom">提交</Button>
<!-- <Button type="primary" @click="prevCycle">{{ prevCycleText }}</Button>-->
<!-- <Button type="primary" @click="nextCycle" :disabled="reportData.offset >= 0">{{ nextCycleText }}</Button>-->
</FormItem>
</Col>
<Col span="4">
<FormItem>
<Button type="primary" class="report-bottom-save">保存</Button>
<!-- <Button type="primary" @click="prevCycle">{{ prevCycleText }}</Button>-->
<!-- <Button type="primary" @click="nextCycle" :disabled="reportData.offset >= 0">{{ nextCycleText }}</Button>-->
</FormItem>
</Col>
</Row>
</Form>
</template>
<script>
import UserInput from "../../../components/UserInput"
const TEditor = () => import('../../../components/TEditor');
export default {
name: "ReportEdit",
components: {
TEditor, UserInput
},
props: {
id: {
default: 0,
}
},
data() {
return {
reportData: {
title: ""
, content: ""
, type: "weekly"
, receive: []
, id: 0
, offset: 0 // -1
},
disabledType: false,
userInputShow: true,
prevCycleText: "",
nextCycleText: "",
};
},
watch: {
id(val) {
if (this.id > 0) this.getDetail(val);
},
},
mounted() {
this.getTemplate();
},
methods: {
initLanguage () {
this.prevCycleText = this.$L("上一周");
this.nextCycleText = this.$L("下一周");
},
handleSubmit: function () {
this.$store.dispatch("call", {
url: 'report/store',
data: this.reportData,
method: 'post',
}).then(({data, msg}) => {
// data
this.reportData.content = "";
this.reportData.receive = [];
this.disabledType = false;
// msg
$A.messageSuccess(msg);
this.$emit("saveSuccess");
}).catch(({msg}) => {
// msg
$A.messageError(msg);
});
},
getTemplate() {
this.$store.dispatch("call", {
url: 'report/template',
data: {
type: this.reportData.type,
offset: this.reportData.offset
},
method: 'get',
}).then(({data, msg}) => {
// data
if (data.id) {
this.getDetail(data.id);
} else {
this.reportData.title = data.title;
this.reportData.content = data.content;
}
// msg
}).catch(({msg}) => {
// msg
$A.messageError(msg);
});
},
typeChange(value) {
//
this.reportData.offset = 0;
if ( value === "weekly" ) {
this.prevCycleText = this.$L("上一周");
this.nextCycleText = this.$L("下一周");
} else {
this.prevCycleText = this.$L("上一天");
this.nextCycleText = this.$L("下一天");
}
if (this.id <= 0)
this.getTemplate();
},
getDetail(reportId) {
this.userInputShow = false;
this.$store.dispatch("call", {
url: 'report/detail',
data: {
id: reportId
},
method: 'get',
}).then(({data, msg}) => {
// data
this.reportData.title = data.title;
this.reportData.content = data.content;
this.reportData.receive = data.receives;
this.reportData.type = data.type_val;
this.reportData.id = reportId;
this.disabledType = true;
this.userInputShow = true;
// msg
}).catch(({msg}) => {
// msg
$A.messageError(msg);
this.userInputShow = true;
});
},
prevCycle() {
this.reportData.offset -= 1;
this.disabledType = false;
this.reReportData();
this.getTemplate();
},
nextCycle() {
// 0
if ( this.reportData.offset < 0 ) {
this.reportData.offset += 1;
}
this.disabledType = false;
this.reReportData();
this.getTemplate();
},
//
getLastSubmitter() {
this.userInputShow = false;
this.$store.dispatch("call", {
url: 'report/last_submitter',
method: 'get',
}).then(({data, msg}) => {
// data
this.reportData.receive = data;
this.userInputShow = true;
// msg
}).catch(({msg}) => {
// msg
$A.messageError(msg);
this.userInputShow = true;
});
},
reReportData() {
this.reportData.title = "";
this.reportData.content = "";
this.reportData.receive = [];
this.reportData.id = 0;
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,172 @@
<template>
<div class="report-list-wrap">
<Row class="reportmy-row report-row-header">
<Col span="2"><p class="reportmy-titles">{{ $L("汇报类型") }}</p></Col>
<Col span="6">
<Select
v-model="reportType"
style="width:95%"
:placeholder="this.$L('全部')"
@on-change="typePick"
>
<Option v-for="item in reportTypeList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
</Col>
<Col span="1"></Col>
<Col span="2"><p class="reportmy-titles">{{ $L("汇报时间") }}</p></Col>
<Col span="6">
<DatePicker
type="daterange"
split-panels
:placeholder="this.$L('请选择时间')"
style="width: 95%;"
@on-change="timePick"
></DatePicker>
</Col>
<Col span="1"></Col>
<Col span="4"><Button type="primary" icon="ios-search" @click="searchTab">{{ $L("搜索") }}</Button></Col>
</Row>
<Table class="tableFill report-row-content" ref="tableRef"
:columns="columns" :data="lists"
:loading="loadIng > 0"
:no-data-text="$L(noDataText)" stripe></Table>
<Page class="page-box report-row-foot" :total="listTotal" :current="listPage" :disabled="loadIng > 0"
@on-change="setPage" @on-page-size-change="setPageSize" :page-size-opts="[10,20,30,50,100]"
placement="top" show-elevator show-sizer show-total transfer />
</div>
</template>
<script>
export default {
name: "ReportMy",
data() {
return {
loadIng: 0,
columns: [],
lists: [],
listPage: 1,
listTotal: 0,
listPageSize: 10,
noDataText: "",
createAt: [],
reportType:'',
reportTypeList:[
{value:"weekly",label:'周报' },
{value:"daily",label:'日报' },
],
}
},
mounted() {
this.getLists();
},
methods: {
initLanguage() {
this.noDataText = this.noDataText || "数据加载中.....";
this.columns = [{
"title": this.$L("名称"),
"key": 'title',
sortable: true,
"minWidth": 120,
}, {
"title": this.$L("类型"),
"key": 'type',
"align": 'center',
sortable: true,
"maxWidth": 80,
}, {
"title": this.$L("汇报时间"),
"key": 'created_at',
"align": 'center',
sortable: true,
"maxWidth": 180,
}, {
"title": "操作",
"key": 'action',
"align": 'right',
"width": 80,
render: (h, params) => {
if (!params.row.id) {
return null;
}
let arr = [
h('ETooltip', {
props: { content: this.$L('编辑'), transfer: true, delay: 600 }
}, [h('Icon', {
props: { type: 'md-create', size: 16 },
style: { margin: '0 3px', cursor: 'pointer' },
on: {
click: () => {
this.$emit("edit", params.row.id);
}
}
})]),
h('ETooltip', {
props: { content: this.$L('查看'), transfer: true, delay: 600 },
style: { position: 'relative' },
}, [h('Icon', {
props: { type: 'md-eye', size: 16 },
style: { margin: '0 3px', cursor: 'pointer' },
on: {
click: () => {
this.$emit("detail", params.row);
}
}
})]),
];
return h('div', arr);
},
}];
},
getLists() {
this.loadIng = 1;
this.$store.dispatch("call", {
url: 'report/my',
data: {
page: this.listPage,
created_at: this.createAt,
type: this.reportType
},
method: 'get',
}).then(({data, msg}) => {
// data
this.lists = data.data;
this.listTotal = data.total;
if ( this.lists.length <= 0 ) {
this.noDataText = this.$L("无数据");
}
// msg
}).catch(({msg}) => {
// msg
$A.messageError(msg);
}).finally( () => {
this.loadIng = 0;
} );
},
setPage(page) {
this.listPage = page;
this.getLists();
},
setPageSize(size) {
if (Math.max($A.runNum(this.listPageSize), 10) !== size) {
this.listPageSize = size;
this.getLists();
}
},
timePick(e){
// console.log(e)
this.createAt = e;
},
typePick(e){
// console.log(e)
},
searchTab() {
this.getLists();
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,194 @@
<template>
<div class="report-list-wrap">
<Row class="reportmy-row report-row-header">
<Col span="2"><p class="reportmy-titles">{{ $L("汇报人") }}</p></Col>
<Col span="4">
<Input style="width:100%" v-model="username" :placeholder="$L('请输入用户名')"/>
</Col>
<Col span="1"></Col>
<Col span="2"><p class="reportmy-titles">{{ $L("汇报类型") }}</p></Col>
<Col span="4">
<Select
v-model="reportType"
style="width:100%"
:placeholder="this.$L('全部')"
@on-change="typePick"
>
<Option v-for="item in reportTypeList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
</Col>
<Col span="1"></Col>
<Col span="2"><p class="reportmy-titles">{{ $L("汇报时间") }}</p></Col>
<Col span='4'>
<DatePicker
type="daterange"
split-panels
:placeholder="this.$L('请选择时间')"
style="width: 100%;"
@on-change="timePick"
></DatePicker>
</Col>
<Col span="1"></Col>
<Col span="3"><Button type="primary" icon="ios-search" @click="searchTab">{{ $L("搜索") }}</Button></Col>
</Row>
<Table class="tableFill report-row-content" ref="tableRef"
:columns="columns" :data="lists"
:loading="loadIng > 0"
:no-data-text="$L(noDataText)" stripe></Table>
<Page class="page-box report-row-foot" :total="listTotal" :current="listPage" :disabled="loadIng > 0"
@on-change="setPage" @on-page-size-change="setPageSize" :page-size-opts="[10,20,30,50,100]"
placement="top" show-elevator show-sizer show-total transfer />
</div>
</template>
<script>
export default {
name: "ReportReceive",
data() {
return {
loadIng: 0,
columns: [],
lists: [],
listPage: 1,
listTotal: 0,
listPageSize: 10,
noDataText: "",
username:'',
reportType:'',
createAt: [],
reportTypeList:[
{value:"weekly",label:'周报' },
{value:"daily",label:'日报' },
],
}
},
mounted() {
this.getLists();
},
methods: {
initLanguage() {
this.noDataText = this.noDataText || "数据加载中.....";
this.columns = [{
"title": this.$L("标题"),
"key": 'title',
"sortable": true,
"minWidth": 120,
render: (h, params) => {
let arr = []
if(params.row.receives_user[0].pivot.read==0){
arr.push(
h('Tag', {
props: { //
color: "orange",
}
}, this.$L("未读")),
h('span',params.row.title)
)
}else {
arr.push(
h('Tag', {
props: { //
color: "lime",
}
}, this.$L("已读")),
h('span',params.row.title)
)
}
return h('div',arr)
}
}, {
"title": this.$L("类型"),
"key": 'type',
"align": 'center',
"sortable": true,
"maxWidth": 80,
}, {
"title": this.$L("接收时间"),
"key": 'receive_time',
"align": 'center',
"sortable": true,
"maxWidth": 180,
}, {
"title": " ",
"key": 'action',
"align": 'right',
"width": 40,
render: (h, params) => {
if (!params.row.id) {
return null;
}
let arr = [
h('ETooltip', {
props: { content: this.$L('查看'), transfer: true, delay: 600 },
style: { position: 'relative' },
}, [h('Icon', {
props: { type: 'md-eye', size: 16 },
style: { margin: '0 3px', cursor: 'pointer' },
on: {
click: () => {
this.$emit("detail", params.row)
this.lists[params.index].receives_user[0].pivot.read = 1
}
}
})])
];
return h('div', arr);
},
}];
},
getLists() {
this.loadIng = 1;
this.$store.dispatch("call", {
url: 'report/receive',
data: {
page: this.listPage,
username: this.username,
created_at: this.createAt,
type: this.reportType
},
method: 'get',
}).then(({data, msg}) => {
// data
this.lists = data.data;
this.listTotal = data.total;
if ( this.lists.length <= 0 ) {
this.noDataText = this.$L("无数据");
}
// msg
}).catch(({msg}) => {
// msg
$A.messageError(msg);
}).finally( () => {
this.loadIng = 0;
} );
},
setPage(page) {
this.listPage = page;
this.getLists();
},
setPageSize(size) {
if (Math.max($A.runNum(this.listPageSize), 10) !== size) {
this.listPageSize = size;
this.getLists();
}
},
timePick(e){
this.createAt = e;
},
typePick(e){
// console.log(e)
},
searchTab() {
this.getLists();
},
}
}
</script>
<style scoped>
</style>

14
resources/assets/sass/_.scss vendored Normal file
View File

@ -0,0 +1,14 @@
@import "app-down";
@import "auto-tip";
@import "circle";
@import "drawer-overlay";
@import "img-update";
@import "loading";
@import "scroller-y";
@import "spinner";
@import "t-editor";
@import "quick-edit";
@import "tag-input";
@import "user-avatar";
@import "user-input";
@import "report";

74
resources/assets/sass/app-down.scss vendored Normal file
View File

@ -0,0 +1,74 @@
.common-app-down {
position: absolute;
bottom: 26px;
right: 26px;
z-index: 1;
display: flex;
align-items: center;
transition: bottom 0.3s;
&.on-client {
&[data-route=login] {
bottom: 75px;
}
}
}
.common-app-down-notification {
.notification-head {
display: flex;
align-items: center;
.notification-title {
display: inline-block;
vertical-align: middle;
font-size: 18px;
color: #17233d;
font-weight: 500;
margin-right: 6px;
}
}
.notification-body {
max-height: 210px;
overflow: auto;
margin: 18px 0;
.markdown-preview {
margin: -20px -12px;
h2 {
font-size: 18px !important;
padding-top: 2px !important;
}
ul {
li {
padding: 2px 0 2px 2px !important;
&:after {
top: 10px !important;
width: 6px !important;
height: 6px !important;
}
}
}
}
}
.notification-link {
margin-top: 20px;
text-align: right;
> button + button {
margin-left: 6px;
}
}
}
.common-app-down-link {
display: inline-block;
cursor: pointer;
line-height: 32px;
height: 32px;
padding: 0 15px;
font-size: 14px;
border-radius: 4px;
color: #fff;
background-color: #8bcf70;
border-color: #8bcf70;
&:hover {
color: #fff;
opacity: 0.9;
}
}

6
resources/assets/sass/auto-tip.scss vendored Normal file
View File

@ -0,0 +1,6 @@
.common-auto-tip {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}

14
resources/assets/sass/circle.scss vendored Normal file
View File

@ -0,0 +1,14 @@
.common-circle {
border-radius: 50%;
.common-circle-path {
fill: transparent;
}
.common-circle-g-path-ring {
stroke: $primary-color;
}
.common-circle-g-path-core {
fill: $primary-color;
transform: scale(0.56);
transform-origin: 50%;
}
}

View File

@ -11,3 +11,4 @@
@import "tag-input";
@import "user-avatar";
@import "user-input";
@import "report";

View File

@ -0,0 +1,218 @@
.report {
height: 100%;
padding: 10px 20px;
.report-list-wrap {
width: 100%;
height: 100%;
display: flex;
top: 0;
padding-top: 53px;
flex-direction: column;
position: absolute;
.report-row-header,.report-row-foot{
flex: 0 0 auto;
}
.report-row-content{
flex:1 1 auto;
.ivu-table{
.ivu-table-body{
height: 100%;
overflow-y: auto;
padding-bottom: 50px;
}
}
}
}
.page-box {
text-align: center;
margin-top: 15px;
}
.ivu-tabs{
height: 100%;
position: relative;
.ivu-tabs-bar{
position: relative;
z-index: 2;
background: #fff;
}
.ivu-tabs-content{
height: 100%;
width: 100%;
margin-top: -53px;
padding-top: 53px;
}
.ivu-tabs-tabpane{
overflow-y: auto;
}
}
}
.report-detail {
.report-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
padding-top: 20px;
.report-title-icon{
font-size: 22px;
cursor: pointer;
margin-right: 20px;
}
}
.report-profile {
font-size: 14px;
}
.report-submitter {
display: flex;
flex-wrap: wrap;
height: 28px;
line-height: 28px;
p{
display: block;
width: 100%;
text-align: justify;
padding-right: 12px;
&:after{
content: '';
display: inline-block;
width: 100%;
}
}
& > div {
margin-right: 8px;
}
}
.ivu-col{
margin-bottom: 12px;
}
.report-content {
margin-top: 12px;
width: 100%;
h2{
margin-bottom: 10px;
}
ol{
margin-bottom: 20px;
padding-left: 18px;
li{
font-size: 14px;
line-height: 24px;
}
}
}
}
.report-box{
display: flex;
flex-direction: column;
height: 100%;
.report-row-header,.report-row-foot{
flex: 0 0 auto;
}
.report-row-content{
flex:1 1 auto;
}
.report-row-foot{
margin-bottom: 0;
}
}
.report-row{
margin-bottom: 20px;
.report-row-a{
float: right;
line-height: 32px;
.report-row-a-icon{
transform: rotate(-90deg);
font-size: 16px;
margin-right: 2px;
}
}
.report-bottom{
width: 120px;
}
.report-bottom-save{
width: 120px;
background: #F4F5F7 ;
color: #515A6E;
border-color: #F4F5F7 ;
}
}
.report-titles{
line-height: 32px;
}
.report-radiogroup{
background: #F4F5F7 !important;
padding: 2px !important;
border-radius: 4px!important;
.ivu-radio-wrapper{
padding: 0 30px !important;
background: #F4F5F7 !important;
color: #515A6E !important;
box-shadow: none !important;
border: none!important;
&:before{
width: 0!important;
}
&:after{
width: 0!important;
}
}
.ivu-radio-focus{
box-shadow: none !important;
border: none!important;
&:after{
background: none!important;
}
}
.ivu-radio-wrapper-checked:not(.ivu-radio-wrapper-disabled){
background: #fff !important;
color: #8BCF70 !important;
box-shadow: none !important;
border: none!important;
border-radius: 4px!important;
}
}
.report-buttongroup{
margin-top: 2px;
background: #F4F5F7!important;
border-radius: 4px;
.report-buttongroup-shu{
position: absolute;
left: 47px;
width: 1px;
height: 23px;
background-color: #E5E5E5;
top: 5px;
}
.ivu-btn-primary{
background: #F4F5F7!important;
box-shadow: none !important;
border: none!important;
color: #8BCF70 !important;
&[disabled]{
color: #515A6E !important;
}
}
}
.report-poptip{
.ivu-tooltip-inner{
min-width: 60px !important;
font-size: 12px !important;
}
}
.reportmy-row{
margin-bottom: 20px;
.reportmy-titles{
line-height: 32px;
}
}
.report-main{
.report-submitter{
padding-top: 13px;
}
}

View File

@ -0,0 +1,136 @@
.drawer-overlay {
position: fixed;
top: 0;
left: 0;
width: 0;
height: 0;
z-index: 1000;
box-sizing: border-box;
pointer-events: none;
background: rgba(0, 0, 0, 0.76);
outline: none;
opacity: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
.overlay-mask {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
.overlay-body {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
z-index: 2;
.overlay-close {
flex-shrink: 0;
display: flex;
align-items: flex-end;
justify-content: flex-end;
> a {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
color: #dbdbde;
&:hover {
color: #fff
}
.icon {
width: 24px;
height: 24px
}
}
}
.overlay-resize {
width: 100%;
height: 5px;
margin-bottom: -5px;
z-index: 1;
}
.overlay-content {
flex: 1;
position: relative;
background: #fff;
border-radius: 18px 18px 0 0;
transform: translate(0, 15%) scale(0.98);
cursor: default;
opacity: 0;
}
}
&.overlay-visible {
pointer-events: auto;
opacity: 1;
width: 100%;
height: 100%;
transition: opacity 0.2s ease;
.overlay-body {
.overlay-content {
opacity: 1;
transform: translate(0, 0) scale(1);
transition: opacity 0.2s ease, transform 0.3s ease;
}
}
}
&.overlay-hide {
width: 100%;
height: 100%;
transition: opacity 0.2s ease;
.overlay-body {
.overlay-content {
transform: translate(0, 15%) scale(0.98);
transition: opacity 0.2s ease, transform 0.2s ease
}
}
}
&.right {
flex-direction: row;
justify-content: flex-end;
.overlay-body {
flex-direction: row;
.overlay-close {
align-items: flex-start;
}
.overlay-resize {
width: 5px;
height: 100%;
margin-right: -5px;
z-index: 1;
}
.overlay-content {
transform: translate(15%, 0) scale(0.98);
border-radius: 18px 0 0 18px;
}
}
&.overlay-visible {
.overlay-body {
.overlay-content {
transform: translate(0, 0) scale(1);
}
}
}
&.overlay-hide {
.overlay-body {
.overlay-content {
transform: translate(15%, 0) scale(0.98);
}
}
}
}
}

255
resources/assets/sass/img-update.scss vendored Normal file
View File

@ -0,0 +1,255 @@
.img-upload-modal {
.ivu-modal-mask {
z-index: 1001;
}
.ivu-modal-no-mask {
background-color: rgba(55, 55, 55, .2);
}
.ivu-modal-wrap {
z-index: 1001;
}
}
.imgcomp-upload-list {
display: inline-block;
width: 60px;
height: 60px;
text-align: center;
line-height: 60px;
border: 1px solid transparent;
border-radius: 4px;
overflow: hidden;
background: #fff;
position: relative;
box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
margin-right: 4px;
vertical-align: top;
.imgcomp-upload-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-position: center;
background-size: cover;
}
.imgcomp-upload-list-cover {
display: none;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, .6);
}
.imgcomp-upload-list-cover i {
color: #fff;
font-size: 24px;
cursor: pointer;
vertical-align: middle;
margin: 0;
transition: all .2s;
}
.imgcomp-upload-list-cover i:hover {
font-size: 28px;
}
.ivu-progress {
height: 100%;
.ivu-progress-outer {
background-color: rgba(0, 0, 0, 0.68);
height: 100%;
.ivu-progress-inner {
width: 88%;
margin: 0 auto;
}
}
}
}
.imgcomp-upload-list:hover .imgcomp-upload-list-cover {
display: block;
}
.img-upload-foot {
display: flex;
align-items: center;
justify-content: flex-end;
.img-upload-foot-input {
flex: 1;
text-align: left;
display: flex;
align-items: center;
justify-content: flex-end;
.img-upload-foot-httptitle {
cursor: pointer;
padding-left: 3px;
margin-right: 22px;
}
}
}
.add-box {
width: 60px;
height: 60px;
line-height: 60px;
display: inline-block;
background: #fff;
border: 1px dashed #dddee1;
border-radius: 4px;
text-align: center;
position: relative;
overflow: hidden;
vertical-align: top;
.add-box-icon {
i {
vertical-align: middle;
padding-bottom: 2px;
}
}
.add-box-upload {
display: none;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
color: #ffffff;
padding-top: 9px;
background: rgba(0, 0, 0, 0.6);
.add-box-item {
height: 22px;
line-height: 22px;
cursor: pointer;
.ivu-upload-drag, .ivu-upload-drag:hover {
background: transparent;
border: 0;
border-radius: 0;
}
span {
transition: all .2s;
font-size: 12px;
}
}
.add-box-item:hover {
span {
font-size: 14px;
}
}
}
em {
font-style: normal;
}
}
.add-box:hover {
border-color: rgba(0, 0, 0, .6);
.add-box-upload {
display: block;
}
}
.callback-add-box {
display: block;
width: auto;
height: 25px;
line-height: 25px;
border: 0;
background: transparent;
.add-box-icon {
display: none;
}
.add-box-upload {
display: block;
width: auto;
background: transparent;
color: #333;
padding: 0;
> div {
display: inline-block;
padding-right: 10px;
}
}
}
.browse-load {
margin: 20px;
text-align: center;
}
.browse-list {
max-height: 540px;
overflow: auto;
.browse-item {
margin: 10px 15px;
display: inline-block;
text-align: center;
cursor: pointer;
position: relative;
.browse-img {
width: 64px;
height: 64px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAABxCAAAAABg5GeyAAACW0lEQVR4Ae3XVRLjMBAE0L3/rSwKM3OcSNPyLVYOLvM6UD0Bd03LVe9XH+RlhlRSSSWVVFJJJZVUUkkllVRSSSWVVFJJJZVUUkl9WyqppJJKKqmkkgpURP17xngOAR5NxW5wlJ9MaLQh83F4NHWmd/gZtdVBaOldfDB1bq5UpJFbFOC6LKnYrkRO209PAw+hIuzWB8Ep5es8HvYo4z4tE1X8UeRwlMM2D5Bzkc7kj6Bi3VTKDDwEeUcrMxrUvGDXTnHa6kK69SDN9sgq1clxKSbNHqqnYmdri81Q9QHf1JPt1Frncaib2XbiTKL2GkHaurnY9LOulMV0O7G6Kw+g9sw2ohhm62KezVJaaufjWC1TnOkr1exilJ7Ji0vxCCqO9V4UwV4PYr9+apouhGYLKfnahdpqegjmeoXOpXgANe70pKT6Zhu19qkY2nC0PZS527lQOyInqr8Uvc5jqfUb1X+PGh5IhW90S2quh3FQC2XRcF66TUkTXPcLKm5FtdR9RJq+2hWII7UpFtmsQLEyzsdJtkxxpr6gLotbUSlV9yeT0Trmzk2XPdUThLYarUbWOY9j04xXQ2u+pMZLYSumGmNUH3HbM9qOAwSHodN2Pks25F2j3aI7+IxqNsB+YLWb16ukSjiW4xNB0gMoMfApBS/XZQgi3p9/5RsiKNKZEOwYFVIF5VyTyD19sbyjIJiNJRZxpNbx2S8sGKvGZNHJBniBu9Wy5WxjGuQFqIAcBHiRGyt4ua5gSCWVVFJJJZVUUkkllVRSSSWVVFJJJZVUUkkllVRSSSWVVFI/AgO0SXIVYHeGAAAAAElFTkSuQmCC);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.browse-title {
display: block;
width: 64px;
margin-top: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.browse-icon {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 64px;
font-size: 36px;
padding-top: 15px;
color: #ffffff;
background-color: rgba(0, 0, 0, 0.5);
}
}
}
.browse-list-disabled {
position: relative;
}
.browse-list-disabled:after {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
z-index: 1;
}

View File

@ -180,6 +180,17 @@
}
}
}
.dialog-action {
align-self: flex-start;
display: flex;
align-items: flex-start;
height: 100%;
> * {
margin: 0 5px;
}
}
&.history {
cursor: pointer;
justify-content: center;

View File

@ -79,6 +79,17 @@
.login-switch {
color: #aaaaaa;
}
.login-input-tips-box{
position: relative;
.login-input-tips{
font-size: 12px;
position: absolute;
left: 0;
bottom: -20px;
color: #c7c7c7;
}
}
}
}
.login-bottom {

58
resources/assets/sass/quick-edit.scss vendored Executable file
View File

@ -0,0 +1,58 @@
.quick-edit {
display: flex;
align-items: center;
max-width: 100%;
.quick-input {
flex: 1;
max-width: 100%;
position: relative;
.quick-loading {
position: absolute;
top: 0;
right: 8px;
bottom: 0;
display: flex;
align-items: center;
.common-loading {
margin: 0;
width: 14px;
height: 14px;
}
}
}
.quick-text {
overflow: hidden;
text-overflow: ellipsis;
align-items: center;
white-space: nowrap;
height: 20px;
line-height: 20px;
margin-right: 6px;
}
.quick-icon {
display: none;
font-size: 16px;
cursor: pointer;
}
&.quick-always {
.quick-icon {
display: inline-block;
opacity: 0.3;
transition: opacity 0.2s;
}
}
&:hover {
.quick-icon {
display: inline-block;
opacity: 1;
}
}
}
.ivu-table-row-hover {
.quick-edit {
.quick-icon {
display: inline-block;
opacity: 1;
}
}
}

218
resources/assets/sass/report.scss vendored Normal file
View File

@ -0,0 +1,218 @@
.report {
height: 100%;
padding: 10px 20px;
.report-list-wrap {
width: 100%;
height: 100%;
display: flex;
top: 0;
padding-top: 53px;
flex-direction: column;
position: absolute;
.report-row-header,.report-row-foot{
flex: 0 0 auto;
}
.report-row-content{
flex:1 1 auto;
.ivu-table{
.ivu-table-body{
height: 100%;
overflow-y: auto;
padding-bottom: 50px;
}
}
}
}
.page-box {
text-align: center;
margin-top: 15px;
}
.ivu-tabs{
height: 100%;
position: relative;
.ivu-tabs-bar{
position: relative;
z-index: 2;
background: #fff;
}
.ivu-tabs-content{
height: 100%;
width: 100%;
margin-top: -53px;
padding-top: 53px;
}
.ivu-tabs-tabpane{
overflow-y: auto;
}
}
}
.report-detail {
.report-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
padding-top: 20px;
.report-title-icon{
font-size: 22px;
cursor: pointer;
margin-right: 20px;
}
}
.report-profile {
font-size: 14px;
}
.report-submitter {
display: flex;
flex-wrap: wrap;
height: 28px;
line-height: 28px;
p{
display: block;
width: 100%;
text-align: justify;
padding-right: 12px;
&:after{
content: '';
display: inline-block;
width: 100%;
}
}
& > div {
margin-right: 8px;
}
}
.ivu-col{
margin-bottom: 12px;
}
.report-content {
margin-top: 12px;
width: 100%;
h2{
margin-bottom: 10px;
}
ol{
margin-bottom: 20px;
padding-left: 18px;
li{
font-size: 14px;
line-height: 24px;
}
}
}
}
.report-box{
display: flex;
flex-direction: column;
height: 100%;
.report-row-header,.report-row-foot{
flex: 0 0 auto;
}
.report-row-content{
flex:1 1 auto;
}
.report-row-foot{
margin-bottom: 0;
}
}
.report-row{
margin-bottom: 20px;
.report-row-a{
float: right;
line-height: 32px;
.report-row-a-icon{
transform: rotate(-90deg);
font-size: 16px;
margin-right: 2px;
}
}
.report-bottom{
width: 120px;
}
.report-bottom-save{
width: 120px;
background: #F4F5F7 ;
color: #515A6E;
border-color: #F4F5F7 ;
}
}
.report-titles{
line-height: 32px;
}
.report-radiogroup{
background: #F4F5F7 !important;
padding: 2px !important;
border-radius: 4px!important;
.ivu-radio-wrapper{
padding: 0 30px !important;
background: #F4F5F7 !important;
color: #515A6E !important;
box-shadow: none !important;
border: none!important;
&:before{
width: 0!important;
}
&:after{
width: 0!important;
}
}
.ivu-radio-focus{
box-shadow: none !important;
border: none!important;
&:after{
background: none!important;
}
}
.ivu-radio-wrapper-checked:not(.ivu-radio-wrapper-disabled){
background: #fff !important;
color: #8BCF70 !important;
box-shadow: none !important;
border: none!important;
border-radius: 4px!important;
}
}
.report-buttongroup{
margin-top: 2px;
background: #F4F5F7!important;
border-radius: 4px;
.report-buttongroup-shu{
position: absolute;
left: 47px;
width: 1px;
height: 23px;
background-color: #E5E5E5;
top: 5px;
}
.ivu-btn-primary{
background: #F4F5F7!important;
box-shadow: none !important;
border: none!important;
color: #8BCF70 !important;
&[disabled]{
color: #515A6E !important;
}
}
}
.report-poptip{
.ivu-tooltip-inner{
min-width: 60px !important;
font-size: 12px !important;
}
}
.reportmy-row{
margin-bottom: 20px;
.reportmy-titles{
line-height: 32px;
}
}
.report-main{
.report-submitter{
padding-top: 13px;
}
}

19
resources/assets/sass/scroller-y.scss vendored Executable file
View File

@ -0,0 +1,19 @@
.app-scroller-y {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.app-scroller-bottom {
height: 0;
margin: 0;
padding: 0;
}
&.static {
position: static;
flex: 1;
}
}

20
resources/assets/sass/spinner.scss vendored Normal file
View File

@ -0,0 +1,20 @@
.common-spinner {
display: none;
position: fixed;
z-index: 9999;
bottom: 20px;
right: 20px;
margin: 0 auto;
width: 30px;
height: 30px;
.common-circular {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
}
}

118
resources/assets/sass/t-editor.scss vendored Executable file
View File

@ -0,0 +1,118 @@
.teditor-box,
.teditor-transfer {
.tox {
&.tox-silver-sink {
z-index: 13000;
}
}
}
.teditor-box {
position: relative;
min-height: 22px;
.icon-inline {
color: #bbbbbb;
position: absolute;
left: 0;
top: 0;
}
textarea {
opacity: 0;
}
.tox-tinymce {
box-shadow: none;
box-sizing: border-box;
border-color: #dddee1;
border-radius: 4px;
overflow: hidden;
.tox-statusbar {
span.tox-statusbar__branding {
a {
display: none;
}
}
}
.tox-tbtn--bespoke {
.tox-tbtn__select-label {
width: auto;
}
}
}
}
.teditor-transfer {
background-color: #ffffff;
.tox-toolbar {
> div:last-child {
> button:last-child {
margin-right: 64px;
}
}
}
.ivu-modal-header {
display: none;
}
.ivu-modal-close {
top: 7px;
z-index: 2;
}
.teditor-transfer-body {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
textarea {
opacity: 0;
}
.tox-tinymce {
border: 0;
.tox-statusbar {
span.tox-statusbar__branding {
a {
display: none;
}
}
}
}
}
}
.teditor-loadstyle {
width: 100%;
height: 180px;
overflow: hidden;
position: relative;
}
.teditor-loadedstyle {
width: 100%;
max-height: inherit;
overflow: inherit;
position: relative;
}
.upload-control {
display: none;
width: 0;
height: 0;
overflow: hidden;
}
.tox-tinymce-inline {
z-index: 100000;
}

93
resources/assets/sass/tag-input.scss vendored Executable file
View File

@ -0,0 +1,93 @@
.common-tag-input {
display: inline-block;
width: 100%;
min-height: 32px;
padding: 2px 7px;
border: 1px solid #dddee1;
border-radius: 4px;
color: #495060;
background: #fff;
position: relative;
cursor: text;
vertical-align: middle;
line-height: normal;
transition: all .2s;
&:hover {
border-color: #a2d98d;
}
&.focus {
border-color: #a2d98d;
box-shadow: 0 0 0 2px rgba(139,207,112,.2)
}
.tags-item, .tags-input {
position: relative;
float: left;
color: #495060;
background-color: #f1f8ff;
border-radius: 3px;
line-height: 22px;
margin: 2px 6px 2px 0;
padding: 0 20px 0 6px;
.tags-content {
line-height: 22px;
}
.tags-del {
width: 20px;
height: 22px;
text-align: center;
cursor: pointer;
position: absolute;
top: -1px;
right: 0;
}
}
.tags-input {
max-width: 80%;
padding: 0;
background-color: inherit;
border: none;
color: inherit;
height: 22px;
line-height: 22px;
-webkit-appearance: none;
outline: none;
resize: none;
overflow: hidden;
}
.tags-input::placeholder {
color: #bbbbbb;
}
.tags-placeholder {
position: absolute;
left: 0;
top: 0;
z-index: -1;
color: rgba(255, 255, 255, 0);
}
}
.common-tag-input::after {
content: "";
display: block;
height: 0;
clear: both;
}
.ivu-form-item-error {
.common-tag-input {
border-color: #ed4014;
&:hover {
border-color: #ed4014;
}
&.focus {
border-color: #ed4014;
box-shadow: 0 0 0 2px rgba(237,64,20,.2)
}
}
}

73
resources/assets/sass/user-avatar.scss vendored Executable file
View File

@ -0,0 +1,73 @@
.common-avatar {
position: relative;
&.avatar-wrapper {
display: flex;
align-items: center;
.avatar-box {
position: relative;
border-radius: 50%;
display: flex;
align-items: center;
.avatar-default {
background-color: transparent;
}
.avatar-text {
background-color: $primary-color;
> span {
display: inline-block;
font-size: 15px;
line-height: 1;
}
}
> em {
position: absolute;
right: 0;
bottom: 0;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ff9900;
border: 1px solid #ffffff;
transform-origin: right bottom;
z-index: 1;
}
&.online {
> em {
background-color: $primary-color;
}
}
}
.avatar-name {
padding-left: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.common-avatar-transfer {
padding: 4px 2px;
line-height: 1.5;
> p {
padding: 1px 2px;
}
.avatar-icons {
margin-top: 12px;
border-top: 1px solid rgba(244, 244, 245, 0.5);
padding: 8px 0 2px;
display: flex;
align-items: center;
> i {
cursor: pointer;
font-size: 22px;
margin-right: 12px;
color: #F4F4F5;
&:last-child {
margin-right: 0;
}
&:hover {
color: #ffffff;
}
}
}
}

70
resources/assets/sass/user-input.scss vendored Executable file
View File

@ -0,0 +1,70 @@
.common-user {
position: relative;
white-space: normal;
.common-user-loading {
position: absolute;
top: 2px;
bottom: 0;
right: 10px;
display: flex;
align-items: center;
.common-loading {
width: 14px;
height: 14px;
}
}
&.hidden-input {
.ivu-select-selection {
padding: 0 4px;
.ivu-select-input {
display: none;
}
}
}
}
.common-user-transfer {
.user-input-option {
display: flex;
align-items: center;
.user-input-avatar {
display: flex;
align-items: center;
.avatar {
width: 26px;
height: 26px;
line-height: 26px;
}
}
.user-input-nickname {
margin-left: 10px;
flex: 1;
}
.user-input-userid {
margin-left: 10px;
font-size: 12px;
color: #cccccc;
transition: margin 0.1s;
}
}
.ivu-select-item {
&.ivu-select-item-selected {
&:after {
top: 8px;
}
.user-input-option {
.user-input-userid {
margin-right: 16px;
}
}
}
}
.user-drop-prepend {
text-align: center;
color: #c5c8ce;
line-height: 20px;
padding-bottom: 5px;
font-size: 12px;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 5px;
}
}

View File

@ -3,6 +3,7 @@
use App\Http\Controllers\Api\DialogController;
use App\Http\Controllers\Api\FileController;
use App\Http\Controllers\Api\ProjectController;
use App\Http\Controllers\Api\ReportController;
use App\Http\Controllers\Api\SystemController;
use App\Http\Controllers\Api\UsersController;
use App\Http\Controllers\IndexController;
@ -39,6 +40,9 @@ Route::prefix('api')->middleware(['webapi'])->group(function () {
// 文件
Route::any('file/{method}', FileController::class);
Route::any('file/{method}/{action}', FileController::class);
// 报告
Route::any('report/{method}', ReportController::class);
Route::any('report/{method}/{action}', ReportController::class);
});
/**