feat(extension): 添加时间戳转换和复制功能
- 新增时间戳与日期互相转换功能 - 实现带反馈的文本复制组件 - 添加时间戳实时更新与控制按钮 - 配置ESLint规则支持React和Web Extensions - 注册background script处理复制权限 - 更新manifest.json配置文件 - 优化CSS样式支持新功能界面 - 移除用户列表页面相关路由和导入 - 添加输入框和选择器样式 - 实现日期格式化工具函数
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"webextensions": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"no-undef": "error"
|
||||
},
|
||||
"globals": {
|
||||
"chrome": "readonly"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === 'start') {
|
||||
navigator.clipboard.writeText(request.text)
|
||||
.then(() => sendResponse({success: true}))
|
||||
.catch((err) => console.log(err));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
+20
-15
@@ -1,19 +1,24 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "React Router Chrome Extension",
|
||||
"version": "1.0",
|
||||
"description": "A Chrome Extension with React Router: User List + Actions Pages",
|
||||
"permissions": ["storage"],
|
||||
"icons": {
|
||||
"16": "favicon.ico",
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "React Router Chrome Extension",
|
||||
"version": "1.0",
|
||||
"description": "A Chrome Extension with React Router: User List + Actions Pages",
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"icons": {
|
||||
"16": "favicon.ico",
|
||||
"48": "favicon.ico",
|
||||
"128": "favicon.ico"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "index.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "index.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
"action": {
|
||||
"default_popup": "index.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "index.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
+69
-36
@@ -1,65 +1,98 @@
|
||||
.app {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
box-shadow: #0b7dda 0 0 0 10px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-list li {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
padding: 10px 20px;
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
padding: 10px 20px;
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #0b7dda;
|
||||
background: #0b7dda;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin: 20px 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-right: 10px;
|
||||
padding: 8px 16px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
padding: 8px 16px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #3d8b40;
|
||||
background: #3d8b40;
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
margin-right: 10px;
|
||||
padding: 8px 16px;
|
||||
background: #f32152;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stop-btn:hover {
|
||||
background: #ff0000;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
+4
-9
@@ -1,5 +1,4 @@
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import UserListPage from './pages/UserListPage';
|
||||
import {HashRouter as Router, Routes, Route} from 'react-router-dom';
|
||||
import ActionsPage from './pages/ActionsPage';
|
||||
import TimestampPage from './pages/TimestampPage';
|
||||
import './App.css';
|
||||
@@ -8,14 +7,10 @@ function App() {
|
||||
return (
|
||||
<Router>
|
||||
<div className="app">
|
||||
<nav className="navbar">
|
||||
<h2>User Manager Extesion</h2>
|
||||
</nav>
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<UserListPage />} />
|
||||
<Route path="/actions" element={<ActionsPage />} />
|
||||
<Route path="/timestamp" element={<TimestampPage />} />
|
||||
<Route path="/" element={<TimestampPage/>}/>
|
||||
<Route path="/actions" element={<ActionsPage/>}/>
|
||||
<Route path="/timestamp" element={<TimestampPage/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import {useState, useCallback} from "react";
|
||||
|
||||
const CopyButton = ({
|
||||
text = '要复制的文本',
|
||||
buttonText = '复制文本',
|
||||
className = 'action-btn',
|
||||
}) => {
|
||||
const [btnText, setBtnText] = useState(buttonText);
|
||||
const [copyStatus, setCopyStatus] = useState('');
|
||||
|
||||
// 重置按钮状态的函数
|
||||
const resetButton = useCallback(() => {
|
||||
setBtnText(buttonText);
|
||||
setCopyStatus('');
|
||||
}, [buttonText]);
|
||||
|
||||
// 复制成功的处理
|
||||
const handleCopySuccess = useCallback(() => {
|
||||
setCopyStatus('success');
|
||||
|
||||
// 2秒后恢复
|
||||
setTimeout(() => {
|
||||
resetButton();
|
||||
}, 2000);
|
||||
}, [text, resetButton]);
|
||||
|
||||
// 复制失败的处理
|
||||
const handleCopyError = useCallback(() => {
|
||||
setCopyStatus('error');
|
||||
|
||||
// 2秒后恢复
|
||||
setTimeout(() => {
|
||||
resetButton();
|
||||
}, 2000);
|
||||
}, [resetButton]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
|
||||
chrome.runtime.sendMessage({
|
||||
action: 'copy',
|
||||
text: text
|
||||
}, (response) => {
|
||||
if (response && response.success) {
|
||||
handleCopySuccess();
|
||||
} else {
|
||||
handleCopyError();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await navigator.clipboard.writeText(text.toString());
|
||||
handleCopySuccess();
|
||||
}
|
||||
} catch (err) {
|
||||
handleCopyError();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={className}
|
||||
style={{
|
||||
backgroundColor: copyStatus === 'success' ? '#4CAF50' :
|
||||
copyStatus === 'error' ? '#f44336' : '',
|
||||
color: copyStatus ? 'white' : '',
|
||||
transition: 'all 0.3s ease',
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{btnText}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyButton;
|
||||
+86
-46
@@ -1,44 +1,76 @@
|
||||
import {useState, useEffect, useRef} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {useState, useEffect, useRef, useCallback, useMemo} from 'react';
|
||||
import CopyButton from "../components/CopyButton";
|
||||
|
||||
function formatDate(value) {
|
||||
return (new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
})).format(value);
|
||||
}
|
||||
|
||||
const TimestampPage = () => {
|
||||
const [currentTimestamp, setCurrentTimestamp] = useState(Math.floor(Date.now()));
|
||||
const [showMilliseconds, setShowMilliseconds] = useState(true);
|
||||
const [isRunningTimestamp, setIsRunningTimestamp] = useState(true);
|
||||
const intervalRef = useRef(null);
|
||||
const [timestampValue, setTimestampValue] = useState(Date.now());
|
||||
const [dateValue, setDateValue] = useState(formatDate(Date.now()));
|
||||
const [timestampResult, setTimestampResult] = useState('');
|
||||
const [dateResult, setDateResult] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (isRunningTimestamp) {
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentTimestamp(Math.floor(Date.now()));
|
||||
}, 100);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
}
|
||||
}, [isRunningTimestamp]);
|
||||
|
||||
|
||||
function handleChangeUnit() {
|
||||
setShowMilliseconds(!showMilliseconds);
|
||||
}
|
||||
|
||||
function handleChangeTimestampRunning(status) {
|
||||
setIsRunningTimestamp(status);
|
||||
if (!status) {
|
||||
console.log('停止时间戳')
|
||||
} else {
|
||||
console.log('开始时间戳')
|
||||
}
|
||||
|
||||
const handleToggleTimestampRunning = useCallback(() => {
|
||||
const newStatus = !isRunningTimestamp;
|
||||
setIsRunningTimestamp(newStatus);
|
||||
console.log(newStatus ? '开始时间戳' : '停止时间戳');
|
||||
}, [isRunningTimestamp]);
|
||||
useMemo(() => ({
|
||||
backgroundColor: isRunningTimestamp ? 'red' : 'green'
|
||||
}), [isRunningTimestamp]);
|
||||
|
||||
function handleConvertTimestampToDate() {
|
||||
setTimestampResult(formatDate(new Date(Number(timestampValue))));
|
||||
}
|
||||
|
||||
|
||||
function handleConvertDateToTimestamp() {
|
||||
if (!dateValue) {
|
||||
setDateResult('请输入日期222');
|
||||
return;
|
||||
}
|
||||
|
||||
const date = new Date(dateValue);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
setDateResult('请输入日期333');
|
||||
return;
|
||||
}
|
||||
|
||||
setDateResult(date.getTime());
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="page-timestamp" class="page">
|
||||
<Link to="/" className="btn">
|
||||
Back to User List
|
||||
</Link>
|
||||
<h2>时间戳工具</h2>
|
||||
<div>
|
||||
<div id="current-timestamp">
|
||||
<h2>当前时间戳</h2>
|
||||
<p>
|
||||
@@ -47,63 +79,71 @@ const TimestampPage = () => {
|
||||
</p>
|
||||
<div>
|
||||
<button className="action-btn" onClick={handleChangeUnit}>切换单位</button>
|
||||
<button className="action-btn">复制</button>
|
||||
<button className="action-btn" onClick={() => handleChangeTimestampRunning(false)}>停止</button>
|
||||
<button className="action-btn" onClick={() => handleChangeTimestampRunning(true)}>开始</button>
|
||||
<CopyButton text={currentTimestamp.toString()} buttonText='复制'></CopyButton>
|
||||
<button className={isRunningTimestamp ? 'stop-btn' : 'action-btn'}
|
||||
onClick={handleToggleTimestampRunning}>
|
||||
{isRunningTimestamp ? '停止' : '开始'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2>时间戳转日期时间</h2>
|
||||
<div class="datetime-box">
|
||||
<div class="input-group">
|
||||
<input type="text" id="timestamp-input" placeholder="请输入时间戳" class="input-text"/>
|
||||
<select id="timestamp-input-unit-select" class="select-box">
|
||||
<div className="datetime-box">
|
||||
<div className="input-group">
|
||||
<input type="text" id="timestamp-input" placeholder="请输入时间戳" className="input-text"
|
||||
value={timestampValue} onChange={(e) => setTimestampValue(e.target.value)}/>
|
||||
<select id="timestamp-input-unit-select" className="select-box">
|
||||
<option value="milliseconds">毫秒(ms)</option>
|
||||
<option value="seconds">秒(s)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<button id="convert-timestamp-to-date-btn" className="action-btn">
|
||||
|
||||
<div className="input-group">
|
||||
<button id="convert-timestamp-to-date-btn" className="action-btn" onClick={handleConvertTimestampToDate}>
|
||||
转换
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="timestamp-conversion-result"
|
||||
placeholder="转换结果"
|
||||
class="input-text"
|
||||
className="input-text"
|
||||
value={timestampResult}
|
||||
readOnly
|
||||
/>
|
||||
<select class="select-box" id="timezone-result"></select>
|
||||
<select className="select-box" id="timezone-result"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2>日期时间转时间戳</h2>
|
||||
<div class="datetime-box">
|
||||
<div class="input-group">
|
||||
<input type="text" id="datetime-input" placeholder="输入日期时间" class="input-text"/>
|
||||
<select class="select-box" id="timezone-input"></select>
|
||||
<div className="datetime-box">
|
||||
<div className="input-group">
|
||||
<input type="text" id="datetime-input" placeholder="输入日期时间" className="input-text" value={dateValue}
|
||||
onChange={(e) => setDateValue(e.target.value)}/>
|
||||
<select className="select-box" id="timezone-input"></select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<button id="convert-date-to-timestamp-btn" className="action-btn">
|
||||
|
||||
<div className="input-group">
|
||||
<button id="convert-date-to-timestamp-btn" className="action-btn" onClick={handleConvertDateToTimestamp}>
|
||||
转换
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="date-conversion-result"
|
||||
placeholder="转换结果"
|
||||
class="input-text"
|
||||
className="input-text"
|
||||
value={dateResult}
|
||||
readOnly
|
||||
/>
|
||||
<select id="date-result-unit-select" class="select-box">
|
||||
<select id="date-result-unit-select" className="select-box">
|
||||
<option value="milliseconds">毫秒(ms)</option>
|
||||
<option value="seconds">秒(s)</option>
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user